mirror of
https://github.com/zadam/trilium.git
synced 2026-02-27 09:03:36 +01:00
Merge 19781bb14cb2304d59bfb2463aaa8e7c228a1dc8 into c42c06d048e148534ba8a572252d8b09675a0aa7
This commit is contained in:
commit
c03bce0dc2
@ -3,7 +3,7 @@ import clsx from "clsx";
|
|||||||
import server from "../../services/server";
|
import server from "../../services/server";
|
||||||
import { TargetedMouseEvent, VNode } from "preact";
|
import { TargetedMouseEvent, VNode } from "preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { Dayjs } from "@triliumnext/commons";
|
import { Dayjs, getWeekInfo, WeekSettings } from "@triliumnext/commons";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
|
|
||||||
interface DateNotesForMonth {
|
interface DateNotesForMonth {
|
||||||
@ -22,6 +22,7 @@ const DAYS_OF_WEEK = [
|
|||||||
|
|
||||||
interface DateRangeInfo {
|
interface DateRangeInfo {
|
||||||
weekNumbers: number[];
|
weekNumbers: number[];
|
||||||
|
weekYears: number[];
|
||||||
dates: Dayjs[];
|
dates: Dayjs[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,19 +37,27 @@ export interface CalendarArgs {
|
|||||||
|
|
||||||
export default function Calendar(args: CalendarArgs) {
|
export default function Calendar(args: CalendarArgs) {
|
||||||
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek");
|
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek");
|
||||||
|
const [ firstWeekOfYear ] = useTriliumOptionInt("firstWeekOfYear");
|
||||||
|
const [ minDaysInFirstWeek ] = useTriliumOptionInt("minDaysInFirstWeek");
|
||||||
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
|
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
|
||||||
|
|
||||||
|
const weekSettings = {
|
||||||
|
firstDayOfWeek: firstDayOfWeekISO,
|
||||||
|
firstWeekOfYear: firstWeekOfYear ?? 0,
|
||||||
|
minDaysInFirstWeek: minDaysInFirstWeek ?? 4
|
||||||
|
};
|
||||||
|
|
||||||
const date = args.date;
|
const date = args.date;
|
||||||
const firstDay = date.startOf('month');
|
const firstDay = date.startOf('month');
|
||||||
const firstDayISO = firstDay.isoWeekday();
|
const firstDayISO = firstDay.isoWeekday();
|
||||||
const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO);
|
const monthInfo = getMonthInformation(date, firstDayISO, weekSettings);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CalendarWeekHeader rawFirstDayOfWeek={rawFirstDayOfWeek} />
|
<CalendarWeekHeader rawFirstDayOfWeek={rawFirstDayOfWeek} />
|
||||||
<div className="calendar-body" data-calendar-area="month">
|
<div className="calendar-body" data-calendar-area="month">
|
||||||
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays info={monthInfo.prevMonth} {...args} />}
|
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays info={monthInfo.prevMonth} weekSettings={weekSettings} {...args} />}
|
||||||
<CurrentMonthDays firstDayOfWeekISO={firstDayOfWeekISO} {...args} />
|
<CurrentMonthDays weekSettings={weekSettings} {...args} />
|
||||||
<NextMonthDays dates={monthInfo.nextMonth.dates} {...args} />
|
<NextMonthDays dates={monthInfo.nextMonth.dates} {...args} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -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 prevMonth = date.subtract(1, 'month').format('YYYY-MM');
|
||||||
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState<DateNotesForMonth>();
|
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState<DateNotesForMonth>();
|
||||||
|
|
||||||
@ -77,27 +86,28 @@ function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { da
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CalendarWeek date={date} weekNumber={weekNumbers[0]} {...args} />
|
<CalendarWeek date={date} weekNumber={weekNumbers[0]} weekYear={weekYears[0]} {...args} />
|
||||||
{dates.map(date => <CalendarDay key={date.toISOString()} date={date} dateNotesForMonth={dateNotesForPrevMonth} className="calendar-date-prev-month" {...args} />)}
|
{dates.map(date => <CalendarDay key={date.toISOString()} date={date} dateNotesForMonth={dateNotesForPrevMonth} className="calendar-date-prev-month" {...args} />)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CurrentMonthDays({ date, firstDayOfWeekISO, ...args }: { date: Dayjs, firstDayOfWeekISO: number } & CalendarArgs) {
|
function CurrentMonthDays({ date, weekSettings, ...args }: { date: Dayjs, weekSettings: WeekSettings } & CalendarArgs) {
|
||||||
let dateCursor = date;
|
let dateCursor = date;
|
||||||
const currentMonth = date.month();
|
const currentMonth = date.month();
|
||||||
const items: VNode[] = [];
|
const items: VNode[] = [];
|
||||||
const curMonthString = date.format('YYYY-MM');
|
const curMonthString = date.format('YYYY-MM');
|
||||||
const [ dateNotesForCurMonth, setDateNotesForCurMonth ] = useState<DateNotesForMonth>();
|
const [ dateNotesForCurMonth, setDateNotesForCurMonth ] = useState<DateNotesForMonth>();
|
||||||
|
const { firstDayOfWeek, firstWeekOfYear, minDaysInFirstWeek } = weekSettings;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${curMonthString}`).then(setDateNotesForCurMonth);
|
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${curMonthString}`).then(setDateNotesForCurMonth);
|
||||||
}, [ date ]);
|
}, [ date ]);
|
||||||
|
|
||||||
while (dateCursor.month() === currentMonth) {
|
while (dateCursor.month() === currentMonth) {
|
||||||
const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO);
|
const { weekYear, weekNumber } = getWeekInfo(dateCursor, weekSettings);
|
||||||
if (dateCursor.isoWeekday() === firstDayOfWeekISO) {
|
if (dateCursor.isoWeekday() === firstDayOfWeek) {
|
||||||
items.push(<CalendarWeek key={`${dateCursor.year()}-W${weekNumber}`} date={dateCursor} weekNumber={weekNumber} {...args}/>)
|
items.push(<CalendarWeek key={`${weekYear}-W${weekNumber}`} date={dateCursor} weekNumber={weekNumber} weekYear={weekYear} {...args}/>)
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(<CalendarDay key={dateCursor.toISOString()} date={dateCursor} dateNotesForMonth={dateNotesForCurMonth} {...args} />)
|
items.push(<CalendarDay key={dateCursor.toISOString()} date={dateCursor} dateNotesForMonth={dateNotesForCurMonth} {...args} />)
|
||||||
@ -141,14 +151,8 @@ function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDat
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumber: number, weekNotes: string[] } & Pick<CalendarArgs, "date" | "onWeekClicked">) {
|
function CalendarWeek({ date, weekNumber, weekYear, weekNotes, onWeekClicked }: { weekNumber: number, weekYear: number, weekNotes: string[] } & Pick<CalendarArgs, "date" | "onWeekClicked">) {
|
||||||
const localDate = date.local();
|
const weekString = `${weekYear}-W${String(weekNumber).padStart(2, '0')}`;
|
||||||
|
|
||||||
// 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')}`;
|
|
||||||
|
|
||||||
if (onWeekClicked) {
|
if (onWeekClicked) {
|
||||||
return (
|
return (
|
||||||
@ -169,33 +173,33 @@ function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumb
|
|||||||
>{weekNumber}</span>);
|
>{weekNumber}</span>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) {
|
export function getMonthInformation(date: Dayjs, firstDayISO: number, weekSettings: WeekSettings) {
|
||||||
return {
|
return {
|
||||||
prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO),
|
prevMonth: getPrevMonthDays(date, firstDayISO, weekSettings),
|
||||||
nextMonth: getNextMonthDays(date, firstDayOfWeekISO)
|
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 prevMonthLastDay = date.subtract(1, 'month').endOf('month');
|
||||||
const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7;
|
const daysToAdd = (firstDayISO - weekSettings.firstDayOfWeek + 7) % 7;
|
||||||
const dates: Dayjs[] = [];
|
const dates: Dayjs[] = [];
|
||||||
|
|
||||||
const firstDay = date.startOf('month');
|
const firstDay = date.startOf('month');
|
||||||
const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO);
|
const { weekYear, weekNumber } = getWeekInfo(firstDay, weekSettings);
|
||||||
|
|
||||||
// Get dates from previous month
|
// Get dates from previous month
|
||||||
for (let i = daysToAdd - 1; i >= 0; i--) {
|
for (let i = daysToAdd - 1; i >= 0; i--) {
|
||||||
dates.push(prevMonthLastDay.subtract(i, 'day'));
|
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 lastDayOfMonth = date.endOf('month');
|
||||||
const lastDayISO = lastDayOfMonth.isoWeekday();
|
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 nextMonthFirstDay = date.add(1, 'month').startOf('month');
|
||||||
const dates: Dayjs[] = [];
|
const dates: Dayjs[] = [];
|
||||||
|
|
||||||
@ -206,16 +210,5 @@ function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo
|
|||||||
dates.push(nextMonthFirstDay.add(i, 'day'));
|
dates.push(nextMonthFirstDay.add(i, 'day'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { weekNumbers: [], dates };
|
return { weekNumbers: [], weekYears: [], 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");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type BNote from "../becca/entities/bnote.js";
|
|||||||
|
|
||||||
import attributeService from "./attributes.js";
|
import attributeService from "./attributes.js";
|
||||||
import cloningService from "./cloning.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 hoistedNoteService from "./hoisted_note.js";
|
||||||
import noteService from "./notes.js";
|
import noteService from "./notes.js";
|
||||||
import optionService from "./options.js";
|
import optionService from "./options.js";
|
||||||
@ -63,7 +63,8 @@ function getJournalNoteTitle(
|
|||||||
rootNote: BNote,
|
rootNote: BNote,
|
||||||
timeUnit: TimeUnit,
|
timeUnit: TimeUnit,
|
||||||
dateObj: Dayjs,
|
dateObj: Dayjs,
|
||||||
number: number
|
number: number,
|
||||||
|
weekYear?: number // Optional: the week year for cross-year weeks
|
||||||
) {
|
) {
|
||||||
const patterns = {
|
const patterns = {
|
||||||
year: rootNote.getOwnedLabelValue("yearPattern") || "{year}",
|
year: rootNote.getOwnedLabelValue("yearPattern") || "{year}",
|
||||||
@ -79,9 +80,14 @@ function getJournalNoteTitle(
|
|||||||
const numberStr = number.toString();
|
const numberStr = number.toString();
|
||||||
const ordinalStr = ordinal(dateObj);
|
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<string, string> = {
|
const allReplacements: Record<string, string> = {
|
||||||
// Common date formats
|
// Common date formats
|
||||||
"{year}": dateObj.format("YYYY"),
|
"{year}": yearForDisplay,
|
||||||
|
|
||||||
// Month related
|
// Month related
|
||||||
"{isoMonth}": dateObj.format("YYYY-MM"),
|
"{isoMonth}": dateObj.format("YYYY-MM"),
|
||||||
@ -286,6 +292,14 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
|||||||
return monthNote as unknown as 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 {
|
function getWeekStartDate(date: Dayjs): Dayjs {
|
||||||
const firstDayISO = parseInt(optionService.getOptionOrNull("firstDayOfWeek") ?? "1", 10);
|
const firstDayISO = parseInt(optionService.getOptionOrNull("firstDayOfWeek") ?? "1", 10);
|
||||||
const day = date.isoWeekday();
|
const day = date.isoWeekday();
|
||||||
@ -294,9 +308,8 @@ function getWeekStartDate(date: Dayjs): Dayjs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWeekNumberStr(date: Dayjs): string {
|
function getWeekNumberStr(date: Dayjs): string {
|
||||||
const isoYear = date.isoWeekYear();
|
const { weekYear, weekNumber } = getWeekInfo(date, getWeekSettings());
|
||||||
const isoWeekNum = date.isoWeek();
|
return `${weekYear}-W${weekNumber.toString().padStart(2, "0")}`;
|
||||||
return `${isoYear}-W${isoWeekNum.toString().padStart(2, "0")}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) {
|
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 [ yearStr, weekNumStr ] = weekStr.trim().split("-W");
|
||||||
const weekNumber = parseInt(weekNumStr);
|
const weekNumber = parseInt(weekNumStr);
|
||||||
|
const weekYear = parseInt(yearStr);
|
||||||
|
|
||||||
const firstDayOfYear = dayjs().year(parseInt(yearStr)).month(0).date(1);
|
// Calculate week start date based on user's first week of year settings.
|
||||||
const weekStartDate = firstDayOfYear.add(weekNumber - 1, "week");
|
// This correctly handles cross-year weeks based on user preferences.
|
||||||
const startDate = getWeekStartDate(weekStartDate);
|
const firstDayOfWeek1 = getFirstDayOfWeek1(weekYear, getWeekSettings());
|
||||||
const endDate = dayjs(startDate).add(6, "day");
|
const startDate = firstDayOfWeek1.add(weekNumber - 1, "week");
|
||||||
|
const endDate = startDate.add(6, "day");
|
||||||
|
|
||||||
const startMonth = startDate.month();
|
const startMonth = startDate.month();
|
||||||
const endMonth = endDate.month();
|
const endMonth = endDate.month();
|
||||||
|
|
||||||
const monthNote = getMonthNote(startDate.format("YYYY-MM-DD"), rootNote);
|
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(() => {
|
sql.transactional(() => {
|
||||||
weekNote = createNote(monthNote, noteTitle);
|
weekNote = createNote(monthNote, noteTitle);
|
||||||
|
|||||||
@ -13,3 +13,4 @@ export * from "./lib/attribute_names.js";
|
|||||||
export * from "./lib/utils.js";
|
export * from "./lib/utils.js";
|
||||||
export * from "./lib/dayjs.js";
|
export * from "./lib/dayjs.js";
|
||||||
export * from "./lib/notes.js";
|
export * from "./lib/notes.js";
|
||||||
|
export * from "./lib/week_utils.js";
|
||||||
|
|||||||
210
packages/commons/src/lib/week_utils.spec.ts
Normal file
210
packages/commons/src/lib/week_utils.spec.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("2026-02-01 (Sunday) should be 2026-W05 (still in week 5)", () => {
|
||||||
|
// Feb 1, 2026 is Sunday - with Monday as first day, this is the last day of week 5
|
||||||
|
// Week 5 starts on 2026-01-26 (Mon) and ends on 2026-02-01 (Sun)
|
||||||
|
const result = getWeekInfo(dayjs("2026-02-01"), settings);
|
||||||
|
expect(result.weekYear).toBe(2026);
|
||||||
|
expect(result.weekNumber).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("2026-02-02 (Monday) should be 2026-W06 (start of week 6)", () => {
|
||||||
|
// Feb 2, 2026 is Monday - start of week 6
|
||||||
|
const result = getWeekInfo(dayjs("2026-02-02"), settings);
|
||||||
|
expect(result.weekYear).toBe(2026);
|
||||||
|
expect(result.weekNumber).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with firstDayOfWeek=7 (Sunday as first day)", () => {
|
||||||
|
const settings: WeekSettings = {
|
||||||
|
firstDayOfWeek: 7, // Sunday
|
||||||
|
firstWeekOfYear: 0,
|
||||||
|
minDaysInFirstWeek: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
it("2026-02-01 (Sunday) should be 2026-W06 (start of new week)", () => {
|
||||||
|
// Feb 1, 2026 is Sunday - should be the START of week 6, not end of week 5
|
||||||
|
const result = getWeekInfo(dayjs("2026-02-01"), settings);
|
||||||
|
expect(result.weekYear).toBe(2026);
|
||||||
|
expect(result.weekNumber).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("2026-01-31 (Saturday) should be 2026-W05 (last day of week 5)", () => {
|
||||||
|
// Jan 31, 2026 is Saturday - should be the last day of week 5
|
||||||
|
const result = getWeekInfo(dayjs("2026-01-31"), settings);
|
||||||
|
expect(result.weekYear).toBe(2026);
|
||||||
|
expect(result.weekNumber).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("2026-01-25 (Sunday) should be 2026-W05 (start of week 5)", () => {
|
||||||
|
// Jan 25, 2026 is Sunday - week 5 starts here
|
||||||
|
const result = getWeekInfo(dayjs("2026-01-25"), settings);
|
||||||
|
expect(result.weekYear).toBe(2026);
|
||||||
|
expect(result.weekNumber).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
143
packages/commons/src/lib/week_utils.ts
Normal file
143
packages/commons/src/lib/week_utils.ts
Normal file
@ -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");
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user