diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-04 22:51:01 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-04 22:51:01 +0900 |
| commit | 2d617f236fa3fbd39a0a7def23199a36cfa0b4c2 (patch) | |
| tree | f6c4afb9d1690e4ee821cb2795ec04e2de1879b8 | |
| parent | 62beeeab74b844b7a7819345c844114428450d96 (diff) | |
| download | kioku-2d617f236fa3fbd39a0a7def23199a36cfa0b4c2.tar.gz kioku-2d617f236fa3fbd39a0a7def23199a36cfa0b4c2.tar.zst kioku-2d617f236fa3fbd39a0a7def23199a36cfa0b4c2.zip | |
fix(study): use 3 AM boundary for counting today's new card reviews
countTodayNewCardReviews was using midnight (0:00) as the start of day,
inconsistent with the 3 AM study day boundary used elsewhere. Reviews
between 0:00-3:00 AM were incorrectly counted as the next day's budget.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | src/server/repositories/review-log.ts | 4 | ||||
| -rw-r--r-- | src/shared/date.test.ts | 49 | ||||
| -rw-r--r-- | src/shared/date.ts | 21 |
3 files changed, 71 insertions, 3 deletions
diff --git a/src/server/repositories/review-log.ts b/src/server/repositories/review-log.ts index 591c647..97488d2 100644 --- a/src/server/repositories/review-log.ts +++ b/src/server/repositories/review-log.ts @@ -1,4 +1,5 @@ import { and, eq, gte, sql } from "drizzle-orm"; +import { getStartOfStudyDayBoundary } from "../../shared/date.js"; import { db } from "../db/index.js"; import { CardState, cards, reviewLogs } from "../db/schema.js"; import type { ReviewLog, ReviewLogRepository } from "./types.js"; @@ -32,8 +33,7 @@ export const reviewLogRepository: ReviewLogRepository = { }, async countTodayNewCardReviews(deckId: string, now: Date): Promise<number> { - const startOfDay = new Date(now); - startOfDay.setHours(0, 0, 0, 0); + const startOfDay = getStartOfStudyDayBoundary(now); const result = await db .select({ count: sql<number>`count(distinct ${reviewLogs.cardId})::int` }) diff --git a/src/shared/date.test.ts b/src/shared/date.test.ts index 1c61a15..da832f5 100644 --- a/src/shared/date.test.ts +++ b/src/shared/date.test.ts @@ -1,5 +1,52 @@ import { describe, expect, it } from "vitest"; -import { getEndOfStudyDayBoundary } from "./date"; +import { getEndOfStudyDayBoundary, getStartOfStudyDayBoundary } from "./date"; + +describe("getStartOfStudyDayBoundary", () => { + it("should return today 3:00 AM when current time is after 3:00 AM", () => { + // Feb 2, 2026 10:00 AM + const now = new Date(2026, 1, 2, 10, 0, 0, 0); + const boundary = getStartOfStudyDayBoundary(now); + + expect(boundary.getFullYear()).toBe(2026); + expect(boundary.getMonth()).toBe(1); + expect(boundary.getDate()).toBe(2); + expect(boundary.getHours()).toBe(3); + expect(boundary.getMinutes()).toBe(0); + expect(boundary.getSeconds()).toBe(0); + }); + + it("should return yesterday 3:00 AM when current time is before 3:00 AM", () => { + // Feb 2, 2026 1:30 AM + const now = new Date(2026, 1, 2, 1, 30, 0, 0); + const boundary = getStartOfStudyDayBoundary(now); + + expect(boundary.getFullYear()).toBe(2026); + expect(boundary.getMonth()).toBe(1); + expect(boundary.getDate()).toBe(1); + expect(boundary.getHours()).toBe(3); + expect(boundary.getMinutes()).toBe(0); + expect(boundary.getSeconds()).toBe(0); + }); + + it("should return today 3:00 AM when current time is exactly 3:00 AM", () => { + // Feb 2, 2026 3:00 AM + const now = new Date(2026, 1, 2, 3, 0, 0, 0); + const boundary = getStartOfStudyDayBoundary(now); + + expect(boundary.getDate()).toBe(2); + expect(boundary.getHours()).toBe(3); + }); + + it("should handle month boundaries correctly", () => { + // Feb 1, 2026 1:00 AM + const now = new Date(2026, 1, 1, 1, 0, 0, 0); + const boundary = getStartOfStudyDayBoundary(now); + + expect(boundary.getMonth()).toBe(0); // January + expect(boundary.getDate()).toBe(31); + expect(boundary.getHours()).toBe(3); + }); +}); describe("getEndOfStudyDayBoundary", () => { it("should return next day 3:00 AM when current time is after 3:00 AM", () => { diff --git a/src/shared/date.ts b/src/shared/date.ts index 583d2a6..3b34f14 100644 --- a/src/shared/date.ts +++ b/src/shared/date.ts @@ -1,4 +1,25 @@ /** + * Returns the start-of-day boundary for the current study day. + * + * The "study day" is defined as 3:00 AM to the next day's 3:00 AM. + * + * - If current time >= 3:00 AM, start = today 3:00 AM local time + * - If current time < 3:00 AM, start = yesterday 3:00 AM local time + */ +export function getStartOfStudyDayBoundary(now: Date = new Date()): Date { + const boundary = new Date(now); + boundary.setMinutes(0, 0, 0); + + if (boundary.getHours() < 3) { + // Move to previous day + boundary.setDate(boundary.getDate() - 1); + } + + boundary.setHours(3); + return boundary; +} + +/** * Returns the end-of-day boundary for due card comparison. * * The "study day" is defined as 3:00 AM to the next day's 3:00 AM. |
