mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 19:45:42 -04:00
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:
2 files changed
+73
-16
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in new issue
Block a user