diff --git a/apps/client/src/widgets/launch_bar/Calendar.tsx b/apps/client/src/widgets/launch_bar/Calendar.tsx
index f081eee8c..8af6628da 100644
--- a/apps/client/src/widgets/launch_bar/Calendar.tsx
+++ b/apps/client/src/widgets/launch_bar/Calendar.tsx
@@ -3,7 +3,7 @@ import clsx from "clsx";
import server from "../../services/server";
import { TargetedMouseEvent, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import { Dayjs } from "@triliumnext/commons";
+import { Dayjs, getWeekInfo, WeekSettings } from "@triliumnext/commons";
import { t } from "../../services/i18n";
interface DateNotesForMonth {
@@ -22,6 +22,7 @@ const DAYS_OF_WEEK = [
interface DateRangeInfo {
weekNumbers: number[];
+ weekYears: number[];
dates: Dayjs[];
}
@@ -36,19 +37,27 @@ export interface CalendarArgs {
export default function Calendar(args: CalendarArgs) {
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek");
+ const [ firstWeekOfYear ] = useTriliumOptionInt("firstWeekOfYear");
+ const [ minDaysInFirstWeek ] = useTriliumOptionInt("minDaysInFirstWeek");
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
+ const weekSettings = {
+ firstDayOfWeek: firstDayOfWeekISO,
+ firstWeekOfYear: firstWeekOfYear ?? 0,
+ minDaysInFirstWeek: minDaysInFirstWeek ?? 4
+ };
+
const date = args.date;
const firstDay = date.startOf('month');
const firstDayISO = firstDay.isoWeekday();
- const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO);
+ const monthInfo = getMonthInformation(date, firstDayISO, weekSettings);
return (
<>
- {firstDayISO !== firstDayOfWeekISO &&
}
-
+ {firstDayISO !== firstDayOfWeekISO &&
}
+
>
@@ -67,7 +76,7 @@ function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }
)
}
-function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { date: Dayjs, info: DateRangeInfo } & CalendarArgs) {
+function PreviousMonthDays({ date, info: { dates, weekNumbers, weekYears }, weekSettings, ...args }: { date: Dayjs, info: DateRangeInfo, weekSettings: WeekSettings } & CalendarArgs) {
const prevMonth = date.subtract(1, 'month').format('YYYY-MM');
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState();
@@ -77,27 +86,28 @@ function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { da
return (
<>
-
+
{dates.map(date => )}
>
)
}
-function CurrentMonthDays({ date, firstDayOfWeekISO, ...args }: { date: Dayjs, firstDayOfWeekISO: number } & CalendarArgs) {
+function CurrentMonthDays({ date, weekSettings, ...args }: { date: Dayjs, weekSettings: WeekSettings } & CalendarArgs) {
let dateCursor = date;
const currentMonth = date.month();
const items: VNode[] = [];
const curMonthString = date.format('YYYY-MM');
const [ dateNotesForCurMonth, setDateNotesForCurMonth ] = useState();
+ const { firstDayOfWeek, firstWeekOfYear, minDaysInFirstWeek } = weekSettings;
useEffect(() => {
server.get(`special-notes/notes-for-month/${curMonthString}`).then(setDateNotesForCurMonth);
}, [ date ]);
while (dateCursor.month() === currentMonth) {
- const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO);
- if (dateCursor.isoWeekday() === firstDayOfWeekISO) {
- items.push()
+ const { weekYear, weekNumber } = getWeekInfo(dateCursor, weekSettings);
+ if (dateCursor.isoWeekday() === firstDayOfWeek) {
+ items.push()
}
items.push()
@@ -141,14 +151,8 @@ function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDat
);
}
-function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumber: number, weekNotes: string[] } & Pick) {
- const localDate = date.local();
-
- // Handle case where week is in between years.
- let year = localDate.year();
- if (localDate.month() === 11 && weekNumber === 1) year++;
-
- const weekString = `${year}-W${String(weekNumber).padStart(2, '0')}`;
+function CalendarWeek({ date, weekNumber, weekYear, weekNotes, onWeekClicked }: { weekNumber: number, weekYear: number, weekNotes: string[] } & Pick) {
+ const weekString = `${weekYear}-W${String(weekNumber).padStart(2, '0')}`;
if (onWeekClicked) {
return (
@@ -169,33 +173,33 @@ function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumb
>{weekNumber});
}
-export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) {
+export function getMonthInformation(date: Dayjs, firstDayISO: number, weekSettings: WeekSettings) {
return {
- prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO),
- nextMonth: getNextMonthDays(date, firstDayOfWeekISO)
+ prevMonth: getPrevMonthDays(date, firstDayISO, weekSettings),
+ nextMonth: getNextMonthDays(date, weekSettings.firstDayOfWeek)
}
}
-function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo {
+function getPrevMonthDays(date: Dayjs, firstDayISO: number, weekSettings: WeekSettings): DateRangeInfo {
const prevMonthLastDay = date.subtract(1, 'month').endOf('month');
- const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7;
+ const daysToAdd = (firstDayISO - weekSettings.firstDayOfWeek + 7) % 7;
const dates: Dayjs[] = [];
const firstDay = date.startOf('month');
- const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO);
+ const { weekYear, weekNumber } = getWeekInfo(firstDay, weekSettings);
// Get dates from previous month
for (let i = daysToAdd - 1; i >= 0; i--) {
dates.push(prevMonthLastDay.subtract(i, 'day'));
}
- return { weekNumbers: [ weekNumber ], dates };
+ return { weekNumbers: [ weekNumber ], weekYears: [ weekYear ], dates };
}
-function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo {
+function getNextMonthDays(date: Dayjs, firstDayOfWeek: number): DateRangeInfo {
const lastDayOfMonth = date.endOf('month');
const lastDayISO = lastDayOfMonth.isoWeekday();
- const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1;
+ const lastDayOfUserWeek = ((firstDayOfWeek + 6 - 1) % 7) + 1;
const nextMonthFirstDay = date.add(1, 'month').startOf('month');
const dates: Dayjs[] = [];
@@ -206,16 +210,5 @@ function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo
dates.push(nextMonthFirstDay.add(i, 'day'));
}
}
- return { weekNumbers: [], dates };
-}
-
-export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number {
- const weekStart = getWeekStartDate(date, firstDayOfWeekISO);
- return weekStart.isoWeek();
-}
-
-function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs {
- const currentISO = date.isoWeekday();
- const diff = (currentISO - firstDayOfWeekISO + 7) % 7;
- return date.clone().subtract(diff, "day").startOf("day");
+ return { weekNumbers: [], weekYears: [], dates };
}
diff --git a/apps/server/src/services/date_notes.ts b/apps/server/src/services/date_notes.ts
index 304c3a5c1..0aa15aa33 100644
--- a/apps/server/src/services/date_notes.ts
+++ b/apps/server/src/services/date_notes.ts
@@ -2,7 +2,7 @@ import type BNote from "../becca/entities/bnote.js";
import attributeService from "./attributes.js";
import cloningService from "./cloning.js";
-import { dayjs, Dayjs } from "@triliumnext/commons";
+import { dayjs, Dayjs, getFirstDayOfWeek1, getWeekInfo, WeekSettings } from "@triliumnext/commons";
import hoistedNoteService from "./hoisted_note.js";
import noteService from "./notes.js";
import optionService from "./options.js";
@@ -63,7 +63,8 @@ function getJournalNoteTitle(
rootNote: BNote,
timeUnit: TimeUnit,
dateObj: Dayjs,
- number: number
+ number: number,
+ weekYear?: number // Optional: the week year for cross-year weeks
) {
const patterns = {
year: rootNote.getOwnedLabelValue("yearPattern") || "{year}",
@@ -79,9 +80,14 @@ function getJournalNoteTitle(
const numberStr = number.toString();
const ordinalStr = ordinal(dateObj);
+ // For week notes, use the weekYear if provided (handles cross-year weeks)
+ const yearForDisplay = (timeUnit === "week" && weekYear !== undefined)
+ ? weekYear.toString()
+ : dateObj.format("YYYY");
+
const allReplacements: Record = {
// Common date formats
- "{year}": dateObj.format("YYYY"),
+ "{year}": yearForDisplay,
// Month related
"{isoMonth}": dateObj.format("YYYY-MM"),
@@ -286,6 +292,14 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
return monthNote as unknown as BNote;
}
+function getWeekSettings(): WeekSettings {
+ return {
+ firstDayOfWeek: parseInt(optionService.getOptionOrNull("firstDayOfWeek") ?? "1", 10),
+ firstWeekOfYear: parseInt(optionService.getOptionOrNull("firstWeekOfYear") ?? "0", 10),
+ minDaysInFirstWeek: parseInt(optionService.getOptionOrNull("minDaysInFirstWeek") ?? "4", 10)
+ };
+}
+
function getWeekStartDate(date: Dayjs): Dayjs {
const firstDayISO = parseInt(optionService.getOptionOrNull("firstDayOfWeek") ?? "1", 10);
const day = date.isoWeekday();
@@ -294,9 +308,8 @@ function getWeekStartDate(date: Dayjs): Dayjs {
}
function getWeekNumberStr(date: Dayjs): string {
- const isoYear = date.isoWeekYear();
- const isoWeekNum = date.isoWeek();
- return `${isoYear}-W${isoWeekNum.toString().padStart(2, "0")}`;
+ const { weekYear, weekNumber } = getWeekInfo(date, getWeekSettings());
+ return `${weekYear}-W${weekNumber.toString().padStart(2, "0")}`;
}
function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) {
@@ -329,17 +342,19 @@ function getWeekNote(weekStr: string, _rootNote: BNote | null = null): BNote | n
const [ yearStr, weekNumStr ] = weekStr.trim().split("-W");
const weekNumber = parseInt(weekNumStr);
+ const weekYear = parseInt(yearStr);
- const firstDayOfYear = dayjs().year(parseInt(yearStr)).month(0).date(1);
- const weekStartDate = firstDayOfYear.add(weekNumber - 1, "week");
- const startDate = getWeekStartDate(weekStartDate);
- const endDate = dayjs(startDate).add(6, "day");
+ // Calculate week start date based on user's first week of year settings.
+ // This correctly handles cross-year weeks based on user preferences.
+ const firstDayOfWeek1 = getFirstDayOfWeek1(weekYear, getWeekSettings());
+ const startDate = firstDayOfWeek1.add(weekNumber - 1, "week");
+ const endDate = startDate.add(6, "day");
const startMonth = startDate.month();
const endMonth = endDate.month();
const monthNote = getMonthNote(startDate.format("YYYY-MM-DD"), rootNote);
- const noteTitle = getJournalNoteTitle(rootNote, "week", startDate, weekNumber);
+ const noteTitle = getJournalNoteTitle(rootNote, "week", startDate, weekNumber, weekYear);
sql.transactional(() => {
weekNote = createNote(monthNote, noteTitle);
diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts
index 1ae730a56..fbcd6da8d 100644
--- a/packages/commons/src/index.ts
+++ b/packages/commons/src/index.ts
@@ -13,3 +13,4 @@ export * from "./lib/attribute_names.js";
export * from "./lib/utils.js";
export * from "./lib/dayjs.js";
export * from "./lib/notes.js";
+export * from "./lib/week_utils.js";
diff --git a/packages/commons/src/lib/week_utils.spec.ts b/packages/commons/src/lib/week_utils.spec.ts
new file mode 100644
index 000000000..4c13f9e10
--- /dev/null
+++ b/packages/commons/src/lib/week_utils.spec.ts
@@ -0,0 +1,165 @@
+import { describe, expect, it } from "vitest";
+import { dayjs } from "./dayjs.js";
+import { getWeekInfo, getFirstDayOfWeek1, getWeekString, WeekSettings, DEFAULT_WEEK_SETTINGS } from "./week_utils.js";
+
+describe("week_utils", () => {
+ describe("getWeekInfo", () => {
+ describe("with firstWeekOfYear=0 (first week contains first day of year)", () => {
+ const settings: WeekSettings = {
+ firstDayOfWeek: 1,
+ firstWeekOfYear: 0,
+ minDaysInFirstWeek: 4
+ };
+
+ it("2025-12-29 should be 2026-W01 (cross-year week)", () => {
+ // 2026-01-01 is Thursday, so the week containing it starts on 2025-12-29 (Monday)
+ // This week should be 2026-W01 because it contains 2026-01-01
+ const result = getWeekInfo(dayjs("2025-12-29"), settings);
+ expect(result.weekYear).toBe(2026);
+ expect(result.weekNumber).toBe(1);
+ });
+
+ it("2026-01-01 should be 2026-W01", () => {
+ const result = getWeekInfo(dayjs("2026-01-01"), settings);
+ expect(result.weekYear).toBe(2026);
+ expect(result.weekNumber).toBe(1);
+ });
+
+ it("2025-12-28 should be 2025-W52", () => {
+ // 2025-12-28 is Sunday, which is the last day of the week starting 2025-12-22
+ const result = getWeekInfo(dayjs("2025-12-28"), settings);
+ expect(result.weekYear).toBe(2025);
+ expect(result.weekNumber).toBe(52);
+ });
+
+ it("2026-01-05 should be 2026-W02", () => {
+ // 2026-01-05 is Monday, start of second week
+ const result = getWeekInfo(dayjs("2026-01-05"), settings);
+ expect(result.weekYear).toBe(2026);
+ expect(result.weekNumber).toBe(2);
+ });
+ });
+
+ describe("with firstWeekOfYear=1 (ISO standard, first week contains first Thursday)", () => {
+ const settings: WeekSettings = {
+ firstDayOfWeek: 1,
+ firstWeekOfYear: 1,
+ minDaysInFirstWeek: 4
+ };
+
+ it("2023-01-01 should be 2022-W52 (Jan 1 is Sunday)", () => {
+ // 2023-01-01 is Sunday, so the week starts on 2022-12-26
+ // Since this week doesn't contain Jan 4, it's 2022-W52
+ const result = getWeekInfo(dayjs("2023-01-01"), settings);
+ expect(result.weekYear).toBe(2022);
+ expect(result.weekNumber).toBe(52);
+ });
+
+ it("2023-01-02 should be 2023-W01 (first Monday)", () => {
+ const result = getWeekInfo(dayjs("2023-01-02"), settings);
+ expect(result.weekYear).toBe(2023);
+ expect(result.weekNumber).toBe(1);
+ });
+ });
+
+ describe("with firstWeekOfYear=2 (minimum days in first week)", () => {
+ // 2026-01-01 is Thursday
+ // The week containing Jan 1 starts on 2025-12-29 (Monday)
+ // This week has 4 days in 2026 (Thu, Fri, Sat, Sun = Jan 1-4)
+
+ describe("with minDaysInFirstWeek=1", () => {
+ const settings: WeekSettings = {
+ firstDayOfWeek: 1,
+ firstWeekOfYear: 2,
+ minDaysInFirstWeek: 1
+ };
+
+ it("2025-12-29 should be 2026-W01 (4 days >= 1 minimum)", () => {
+ // Week has 4 days in 2026, which is >= 1
+ const result = getWeekInfo(dayjs("2025-12-29"), settings);
+ expect(result.weekYear).toBe(2026);
+ expect(result.weekNumber).toBe(1);
+ });
+
+ it("2026-01-01 should be 2026-W01", () => {
+ const result = getWeekInfo(dayjs("2026-01-01"), settings);
+ expect(result.weekYear).toBe(2026);
+ expect(result.weekNumber).toBe(1);
+ });
+ });
+
+ describe("with minDaysInFirstWeek=7", () => {
+ const settings: WeekSettings = {
+ firstDayOfWeek: 1,
+ firstWeekOfYear: 2,
+ minDaysInFirstWeek: 7
+ };
+
+ it("2025-12-29 should be 2025-W52 (4 days < 7 minimum, so this is last week of 2025)", () => {
+ // Week has only 4 days in 2026, which is < 7
+ // So this week belongs to 2025
+ const result = getWeekInfo(dayjs("2025-12-29"), settings);
+ expect(result.weekYear).toBe(2025);
+ expect(result.weekNumber).toBe(52);
+ });
+
+ it("2026-01-01 should be 2025-W52 (still last week of 2025)", () => {
+ const result = getWeekInfo(dayjs("2026-01-01"), settings);
+ expect(result.weekYear).toBe(2025);
+ expect(result.weekNumber).toBe(52);
+ });
+
+ it("2026-01-05 should be 2026-W01 (first full week of 2026)", () => {
+ // 2026-01-05 is Monday, start of the first full week
+ const result = getWeekInfo(dayjs("2026-01-05"), settings);
+ expect(result.weekYear).toBe(2026);
+ expect(result.weekNumber).toBe(1);
+ });
+ });
+ });
+ });
+
+ describe("getFirstDayOfWeek1", () => {
+ it("with firstWeekOfYear=0, returns the first day of the week containing Jan 1", () => {
+ const settings: WeekSettings = {
+ firstDayOfWeek: 1,
+ firstWeekOfYear: 0,
+ minDaysInFirstWeek: 4
+ };
+ // 2026-01-01 is Thursday, so week starts on 2025-12-29
+ const result = getFirstDayOfWeek1(2026, settings);
+ expect(result.format("YYYY-MM-DD")).toBe("2025-12-29");
+ });
+
+ it("with firstWeekOfYear=1, returns the first day of the week containing Jan 4", () => {
+ const settings: WeekSettings = {
+ firstDayOfWeek: 1,
+ firstWeekOfYear: 1,
+ minDaysInFirstWeek: 4
+ };
+ // 2023-01-04 is Wednesday, so week starts on 2023-01-02
+ const result = getFirstDayOfWeek1(2023, settings);
+ expect(result.format("YYYY-MM-DD")).toBe("2023-01-02");
+ });
+ });
+
+ describe("getWeekString", () => {
+ it("generates correct week string for cross-year week", () => {
+ const settings: WeekSettings = {
+ firstDayOfWeek: 1,
+ firstWeekOfYear: 0,
+ minDaysInFirstWeek: 4
+ };
+ expect(getWeekString(dayjs("2025-12-29"), settings)).toBe("2026-W01");
+ });
+
+ it("generates correct week string with padded week number", () => {
+ const settings: WeekSettings = {
+ firstDayOfWeek: 1,
+ firstWeekOfYear: 0,
+ minDaysInFirstWeek: 4
+ };
+ expect(getWeekString(dayjs("2026-01-05"), settings)).toBe("2026-W02");
+ });
+ });
+});
diff --git a/packages/commons/src/lib/week_utils.ts b/packages/commons/src/lib/week_utils.ts
new file mode 100644
index 000000000..db7cf4134
--- /dev/null
+++ b/packages/commons/src/lib/week_utils.ts
@@ -0,0 +1,143 @@
+import { dayjs, Dayjs } from "./dayjs.js";
+
+/**
+ * Week settings for calculating week numbers.
+ */
+export interface WeekSettings {
+ /** First day of the week (1=Monday to 7=Sunday) */
+ firstDayOfWeek: number;
+ /**
+ * How to determine the first week of the year:
+ * - 0: First week contains first day of the year
+ * - 1: First week contains first Thursday (ISO 8601 standard)
+ * - 2: First week has minimum days
+ */
+ firstWeekOfYear: number;
+ /** Minimum days in first week (used when firstWeekOfYear=2) */
+ minDaysInFirstWeek: number;
+}
+
+/**
+ * Default week settings (first week contains first day of year, week starts on Monday).
+ */
+export const DEFAULT_WEEK_SETTINGS: WeekSettings = {
+ firstDayOfWeek: 1,
+ firstWeekOfYear: 0,
+ minDaysInFirstWeek: 4
+};
+
+/**
+ * Gets the first day of week 1 for a given year, based on user settings.
+ *
+ * @param year The year to calculate for
+ * @param settings Week calculation settings
+ * @returns The first day of week 1
+ */
+export function getFirstDayOfWeek1(year: number, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): Dayjs {
+ const { firstDayOfWeek, firstWeekOfYear, minDaysInFirstWeek } = settings;
+
+ const jan1 = dayjs(`${year}-01-01`);
+ const jan1Weekday = jan1.isoWeekday(); // 1=Monday, 7=Sunday
+
+ // Calculate the first day of the week containing Jan 1
+ const daysToSubtract = (jan1Weekday - firstDayOfWeek + 7) % 7;
+ const weekContainingJan1Start = jan1.subtract(daysToSubtract, "day");
+
+ if (firstWeekOfYear === 0) {
+ // First week contains first day of the year
+ return weekContainingJan1Start;
+ } else if (firstWeekOfYear === 1) {
+ // First week contains first Thursday (ISO 8601 standard)
+ const jan4 = dayjs(`${year}-01-04`);
+ const jan4Weekday = jan4.isoWeekday();
+ const daysToSubtractFromJan4 = (jan4Weekday - firstDayOfWeek + 7) % 7;
+ return jan4.subtract(daysToSubtractFromJan4, "day");
+ } else {
+ // First week has minimum days
+ const daysInFirstWeek = 7 - daysToSubtract;
+ if (daysInFirstWeek >= minDaysInFirstWeek) {
+ return weekContainingJan1Start;
+ } else {
+ return weekContainingJan1Start.add(1, "week");
+ }
+ }
+}
+
+/**
+ * Gets the week year and week number for a given date based on user settings.
+ *
+ * @param date The date to calculate week info for
+ * @param settings Week calculation settings
+ * @returns Object with weekYear and weekNumber
+ */
+export function getWeekInfo(date: Dayjs, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): { weekYear: number; weekNumber: number } {
+ const { firstDayOfWeek } = settings;
+
+ // Get the start of the week containing this date
+ const dateWeekday = date.isoWeekday();
+ const daysToSubtract = (dateWeekday - firstDayOfWeek + 7) % 7;
+ const weekStart = date.subtract(daysToSubtract, "day");
+
+ // Try current year first
+ let year = date.year();
+ let firstDayOfWeek1 = getFirstDayOfWeek1(year, settings);
+
+ // If the week start is before week 1 of current year, it belongs to previous year
+ if (weekStart.isBefore(firstDayOfWeek1)) {
+ year--;
+ firstDayOfWeek1 = getFirstDayOfWeek1(year, settings);
+ } else {
+ // Check if this might belong to next year's week 1
+ const nextYearFirstDayOfWeek1 = getFirstDayOfWeek1(year + 1, settings);
+ if (!weekStart.isBefore(nextYearFirstDayOfWeek1)) {
+ year++;
+ firstDayOfWeek1 = nextYearFirstDayOfWeek1;
+ }
+ }
+
+ // Calculate week number
+ const weekNumber = weekStart.diff(firstDayOfWeek1, "week") + 1;
+
+ return { weekYear: year, weekNumber };
+}
+
+/**
+ * Generates a week string in the format "YYYY-Www" (e.g., "2026-W01").
+ *
+ * @param date The date to generate the week string for
+ * @param settings Week calculation settings
+ * @returns Week string in format "YYYY-Www"
+ */
+export function getWeekString(date: Dayjs, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): string {
+ const { weekYear, weekNumber } = getWeekInfo(date, settings);
+ return `${weekYear}-W${weekNumber.toString().padStart(2, "0")}`;
+}
+
+/**
+ * Gets the start date of the week containing the given date.
+ *
+ * @param date The date to find the week start for
+ * @param firstDayOfWeek First day of the week (1=Monday to 7=Sunday)
+ * @returns The start of the week
+ */
+export function getWeekStartDate(date: Dayjs, firstDayOfWeek: number = 1): Dayjs {
+ const dateWeekday = date.isoWeekday();
+ const diff = (dateWeekday - firstDayOfWeek + 7) % 7;
+ return date.clone().subtract(diff, "day").startOf("day");
+}
+
+/**
+ * Parses a week string and returns the start date of that week.
+ *
+ * @param weekStr Week string in format "YYYY-Www" (e.g., "2026-W01")
+ * @param settings Week calculation settings
+ * @returns The start date of the week
+ */
+export function parseWeekString(weekStr: string, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): Dayjs {
+ const [yearStr, weekNumStr] = weekStr.trim().split("-W");
+ const weekNumber = parseInt(weekNumStr);
+ const weekYear = parseInt(yearStr);
+
+ const firstDayOfWeek1 = getFirstDayOfWeek1(weekYear, settings);
+ return firstDayOfWeek1.add(weekNumber - 1, "week");
+}