In-process read surface: albums, photos, and timeline grouping #64

Merged
clawbot merged 3 commits from issue-44-read-surface into next 2026-09-22 17:21:48 +02:00
2 changed files with 32 additions and 6 deletions
Showing only changes of commit 555badc0b9 - Show all commits
+28 -3
View File
@@ -151,8 +151,8 @@ export interface PhotoFilter {
} }
export interface TimelineGroup { export interface TimelineGroup {
// The period's identity: `YYYY-MM-DD` for day and week (the Monday), and // The period's identity: `YYYY-MM-DD` for day, `YYYY-Www` (ISO 8601 week,
// `YYYY-MM` for month. // e.g. `2025-W32`) for week, and `YYYY-MM` for month.
key: string; key: string;
// Local-time milliseconds at the start of the period. // Local-time milliseconds at the start of the period.
startsAt: number; startsAt: number;
@@ -292,6 +292,31 @@ const pad = (n: number): string => String(n).padStart(2, "0");
const dateKey = (d: Date): string => const dateKey = (d: Date): string =>
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; `${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 // 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 // Monday. `Date` normalizes out-of-range day arguments, so the week's Monday
// is correct across month and year boundaries. // 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. // getDay(): 0=Sunday..6=Saturday; shift so Monday is the week start.
const fromMonday = (d.getDay() + 6) % 7; const fromMonday = (d.getDay() + 6) % 7;
const start = new Date(year, month, day - fromMonday); 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); const start = new Date(year, month, day);
return { key: dateKey(start), startsAt: start.getTime() }; 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" }); const groups = apis(records).timeline.groups({ groupBy: "week" });
expect(groups.map((g) => g.key)).toEqual(["2024-01-22", "2024-01-15"]); // ISO week keys: 2024-01-15 is in 2024-W03, the next Monday in 2024-W04.
const first = groups.find((g) => g.key === "2024-01-15")!; 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)); expect(first.startsAt).toBe(Date.UTC(2024, 0, 15));
// The Sunday belongs to the Monday-started week, not the next one. // The Sunday belongs to the Monday-started week, not the next one.
expect(first.fileIDs.sort((a, b) => a - b)).toEqual([1, 2, 3]); 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)) })], [file(1, 1, { creationTime: micros(Date.UTC(2024, 0, 14, 12)) })],
); );
const groups = apis(records).timeline.groups({ groupBy: "week" }); 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)); expect(groups[0]!.startsAt).toBe(Date.UTC(2024, 0, 8));
}); });