· 2 min read

SM-2 and day boundaries: why the schedule is stored as dates

How flashcards-sm2 and habit-tracker avoid shifts from time zones and daylight saving: dates instead of timestamps and a movable day boundary.

Project: flashcards-sm2

Spaced repetition lives in days: "review in 6 days". Store a moment in time and a flight to another zone or a daylight saving change will shift a card by a day.

Dates, not moments

In flashcards-sm2 the fields due, last and firstDay are local days YYYY-MM-DD, null for a new card. "Today" is the device calendar date. The schedule knows nothing about hours, so a zone offset does not touch it. The day boundary can be moved to 03:00–05:00 in settings so that an evening session after midnight still counts as yesterday.

The algorithm

A grade q from 0 to 5; the buttons are "Again" = 1, "Hard" = 3, "Good" = 4, "Easy" = 5.

  • EF' = EF + 0.1 − (5−q)(0.08 + (5−q)·0.02), start 2.5, floor 1.3.
  • Success (q ≥ 3): 1 day, then 6 days, then max(prev+1, round(prev·EF')), capped at 3650 days.
  • Failure: the streak resets, the interval is 1, the card returns to the end of the session, EF does not change.

Differences from the original are documented: the interval strictly grows, a 4-day graduating interval is added for "Easy", being overdue does not affect the interval.

The same trick in the habit tracker

In habit-tracker a day is stored as a string in the user's zone at the moment of marking, and arithmetic goes through UTC midnight. So travel and daylight saving do not change history.

Checks and limits

30 vitest tests, some on fast-check. These are concepts: there is no sync between devices, data lives in the browser's IndexedDB.