Key timeline weeks by ISO week, not the Monday date
check / check (push) Successful in 16s

periodOf keyed a week by the Monday's YYYY-MM-DD, which collides with
the day-key form. Weeks now key as YYYY-Www (ISO 8601, e.g. 2025-W32);
the week-year comes from the week's Thursday, so it stays correct across
the January/December boundary. The Monday-start boundary and startsAt
are unchanged. Week tests now assert the ISO form.

Model: opus-4-8
This commit is contained in:
2026-09-22 15:03:43 +00:00
parent 1df0e52d40
commit 555badc0b9
2 changed files with 32 additions and 6 deletions
+28 -3
View File
@@ -151,8 +151,8 @@ export interface PhotoFilter {
}
export interface TimelineGroup {
// The period's identity: `YYYY-MM-DD` for day and week (the Monday), and
// `YYYY-MM` for month.
// The period's identity: `YYYY-MM-DD` for day, `YYYY-Www` (ISO 8601 week,
// e.g. `2025-W32`) for week, and `YYYY-MM` for month.
key: string;
// Local-time milliseconds at the start of the period.
startsAt: number;
@@ -292,6 +292,31 @@ const pad = (n: number): string => String(n).padStart(2, "0");
const dateKey = (d: Date): string =>
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
// The ISO 8601 week key `YYYY-Www` for the week starting at the given Monday.
// The week-year is the year of that week's Thursday, so it can differ from the
// calendar year at the January/December boundary (e.g. 2024-12-30 is 2025-W01).
const isoWeekKey = (monday: Date): string => {
const thursday = new Date(
monday.getFullYear(),
monday.getMonth(),
monday.getDate() + 3,
);
const isoYear = thursday.getFullYear();
// Thursday of ISO week 1 is the Thursday of the week containing January 4.
const jan4 = new Date(isoYear, 0, 4);
const week1Thursday = new Date(
isoYear,
0,
4 + 3 - ((jan4.getDay() + 6) % 7),
);
const week =
1 +
Math.round((thursday.getTime() - week1Thursday.getTime()) / WEEK_MS);
return `${isoYear}-W${pad(week)}`;
};
// The period a millisecond instant falls in, in local time. Weeks start on
// Monday. `Date` normalizes out-of-range day arguments, so the week's Monday
// is correct across month and year boundaries.
@@ -312,7 +337,7 @@ const periodOf = (
// getDay(): 0=Sunday..6=Saturday; shift so Monday is the week start.
const fromMonday = (d.getDay() + 6) % 7;
const start = new Date(year, month, day - fromMonday);
return { key: dateKey(start), startsAt: start.getTime() };
return { key: isoWeekKey(start), startsAt: start.getTime() };
}
const start = new Date(year, month, day);
return { key: dateKey(start), startsAt: start.getTime() };
+4 -3
View File
@@ -287,8 +287,9 @@ describe("lib.timeline grouping", () => {
],
);
const groups = apis(records).timeline.groups({ groupBy: "week" });
expect(groups.map((g) => g.key)).toEqual(["2024-01-22", "2024-01-15"]);
const first = groups.find((g) => g.key === "2024-01-15")!;
// ISO week keys: 2024-01-15 is in 2024-W03, the next Monday in 2024-W04.
expect(groups.map((g) => g.key)).toEqual(["2024-W04", "2024-W03"]);
const first = groups.find((g) => g.key === "2024-W03")!;
expect(first.startsAt).toBe(Date.UTC(2024, 0, 15));
// The Sunday belongs to the Monday-started week, not the next one.
expect(first.fileIDs.sort((a, b) => a - b)).toEqual([1, 2, 3]);
@@ -301,7 +302,7 @@ describe("lib.timeline grouping", () => {
[file(1, 1, { creationTime: micros(Date.UTC(2024, 0, 14, 12)) })],
);
const groups = apis(records).timeline.groups({ groupBy: "week" });
expect(groups.map((g) => g.key)).toEqual(["2024-01-08"]);
expect(groups.map((g) => g.key)).toEqual(["2024-W02"]);
expect(groups[0]!.startsAt).toBe(Date.UTC(2024, 0, 8));
});