fix(stats): align count windows with chart buckets and fix month rollover

Window starts now equal the first chart bucket, and year buckets are built from the 1st of each month so the 29th-31st no longer repeat or skip months.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-05 22:31:32 -04:00
1 parent fcd8731a6a
commit 6b56e3eff3
2 files changed
+73 -16

No files matched your search

+11 -16
View File
@@ -29,24 +29,20 @@ import { getDisplayStatusesByTitleIds } from "./tracking";
export type TimePeriod = "today" | "this_week" | "this_month" | "this_year";
export function periodStartTimestamp(period: TimePeriod): number {
const now = new Date();
const start = new Date(now);
export function periodStartTimestamp(period: TimePeriod, now: Date = new Date()): number {
let start: Date;
switch (period) {
case "today":
start.setHours(now.getHours() - 24, now.getMinutes(), 0, 0);
case "today": // 24 hourly buckets, oldest = the hour 23 hours ago
start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours() - 23);
break;
case "this_week":
start.setDate(now.getDate() - 7);
start.setHours(0, 0, 0, 0);
case "this_week": // 7 daily buckets incl. today
start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
break;
case "this_month":
start.setDate(now.getDate() - 30);
start.setHours(0, 0, 0, 0);
case "this_month": // 30 daily buckets incl. today
start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 29);
break;
case "this_year":
start.setFullYear(now.getFullYear() - 1);
start.setHours(0, 0, 0, 0);
case "this_year": // 12 monthly buckets incl. this month
start = new Date(now.getFullYear(), now.getMonth() - 11, 1);
break;
}
return Math.floor(start.getTime() / 1000);
@@ -117,8 +113,7 @@ export function getWatchHistory(
case "this_year":
fmt = "%Y-%m";
buckets = Array.from({ length: 12 }, (_, i) => {
const d = new Date(now);
d.setMonth(now.getMonth() - 11 + i);
const d = new Date(now.getFullYear(), now.getMonth() - 11 + i, 1);
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}`;
});
break;
+62
View File
@@ -23,6 +23,7 @@ import {
getUserStats,
getWatchCount,
getWatchHistory,
periodStartTimestamp,
} from "../src/discovery";
const TEST_NOW = new Date("2026-03-01T12:00:00Z");
@@ -37,6 +38,7 @@ afterAll(() => {
});
beforeEach(() => {
vi.setSystemTime(TEST_NOW);
clearAllTables();
});
@@ -119,6 +121,66 @@ describe("getWatchHistory", () => {
});
});
describe("getWatchHistory year buckets", () => {
test("yields 12 distinct consecutive months on Oct 31", () => {
vi.setSystemTime(new Date(2026, 9, 31, 12));
insertUser();
expect(getWatchHistory("user-1", "movies", "this_year").map((b) => b.bucket)).toEqual([
"2025-11",
"2025-12",
"2026-01",
"2026-02",
"2026-03",
"2026-04",
"2026-05",
"2026-06",
"2026-07",
"2026-08",
"2026-09",
"2026-10",
]);
});
test("yields 12 distinct consecutive months on Mar 30", () => {
vi.setSystemTime(new Date(2027, 2, 30, 12));
insertUser();
expect(getWatchHistory("user-1", "movies", "this_year").map((b) => b.bucket)).toEqual([
"2026-04",
"2026-05",
"2026-06",
"2026-07",
"2026-08",
"2026-09",
"2026-10",
"2026-11",
"2026-12",
"2027-01",
"2027-02",
"2027-03",
]);
});
});
describe("window boundaries match chart buckets", () => {
test.each(["today", "this_week", "this_month", "this_year"] as const)(
"%s count equals chart sum at the window edge",
(period) => {
const now = new Date(2026, 6, 15, 12, 30);
vi.setSystemTime(now);
insertUser();
insertTitle({ id: "inside", tmdbId: 1 });
insertTitle({ id: "outside", tmdbId: 2 });
const startMs = periodStartTimestamp(period, now) * 1000;
insertMovieWatch("user-1", "inside", new Date(startMs + 60_000));
insertMovieWatch("user-1", "outside", new Date(startMs - 60_000));
expect(getWatchCount("user-1", "movies", period)).toBe(1);
const sum = getWatchHistory("user-1", "movies", period).reduce((n, b) => n + b.count, 0);
expect(sum).toBe(1);
},
);
});
// ── getUserStats ────────────────────────────────────────────────────
describe("getUserStats", () => {