rgoussu@goussu: ~/library/fundamentals/exercises
~/library/fundamentals/exercises cat dst-safe-scheduler.md

DST-safe scheduler

# Hand-parse RFC 3339, model instant vs. civil time with explicit gap/overlap handling, store a future meeting that survives a tzdb update, and expand "daily at 2:30" across both DST edges.

Exercisesaved 2026-08-09source #time#timezones#datetime#tzdb#rrule#fundamentals#exercise

Goal

Build a small scheduling core — timestamp parser, instant/civil-time model, future-event store, and recurrence expander — where every classic time bug is forced into the open and killed by a test. Time's type system (instant vs. civil vs. zoned, duration vs. period) stops being advice and becomes compiler-checked, test-proven muscle memory.

Subject: full brief & instructions

Practices

  • Time, timezones & datetime — the type system of time, the future-civil-event storage rule, DST gaps and overlaps, clock injection; this exercise is that note's drills consolidated into one build.
  • Testing strategies — the injected Clock and deterministic fixtures are the testability discipline applied to time.

Milestones

  1. RFC 3339 parser. Hand-parse 2026-08-08T10:00:00+02:00 (and friends) into civil components + offset without the language's datetime parser; reject the invalid. Show an offset round-trip loses the zone — +02:00 is a moment's offset, not Europe/Paris.
  2. The type system, as types. Distinct Instant, LocalDateTime, and ZonedDateTime types, plus Duration (physical seconds) vs. Period (calendar units). Conversion local + zone → instant runs against injectable zone rules and forces a resolution strategy where the mapping is a gap (0 candidates) or an overlap (2 candidates) — no silent default.
  3. The future meeting. Store "09:00 in Paris, next June" as local time + IANA zone, never a baked UTC instant. Drill: swap in a changed rule set (a government abolishes DST) and show your stored meeting still fires at 09:00 wall time — while a UTC-baked copy is now an hour wrong.
  4. DST-safe recurrence expander. Expand an RRULE subset ("daily/weekly at HH:MM local") across the spring-forward gap (2:30 doesn't exist) and the fall-back overlap (2:30 happens twice), under explicit, named policies for each. Demonstrate period ≠ duration: "+24 hours" drifts across a transition; "+1 day" doesn't.
  5. The hostile-zone suite. Inject the clock, then run the acceptance fixtures: Pacific/Kiritimati (UTC+14), Asia/Kolkata (+05:30), both US and EU DST edges, and a year of "daily digest at 02:30" with zero drift and no double-fires.

Stretch goals

  • Australia/Lord_Howe — the 30-minute DST shift that breaks "DST is always one hour".
  • Calendar arithmetic edges: Jan 31 + 1 month, leap-day birthdays on recurring events.
  • Emit RFC 9557 (2026-06-12T09:00:00+02:00[Europe/Paris]) so serialization stops losing the zone.

Related