Pure-reference catalog of Daylight Saving Time (DST) transition patterns and their canonical bug classes. Covers the spring-forward (skipped hour: 02:00 → 03:00 local) and fall-back (repeated hour: 02:00 → 01:00 local) transitions, the historical irregularity of DST (different jurisdictions, transitions on different dates, some regions abolish DST or never adopted it), the IANA timezone database (tz / Olson DB) as the canonical source, and the testable behaviors DST creates (duplicate / missing local timestamps, cron jobs that fire 0 or 2 times, billing periods that miss / double-count, recurring meetings on transition days). Per-jurisdiction DST-rule tables and refreshable per-region test-data fixtures live in references/. Use when designing or auditing time-handling code or test cases.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
DST transitions cause a large share of production time-bugs: spring-forward creates non-existent local times, fall-back creates duplicate local times. The IANA Time Zone Database (iana.org/time-zones) is the canonical source of historical and current DST rules.
Per en.wikipedia.org/wiki/Daylight_saving_time, in US Eastern: on the 2nd Sunday of March, at 02:00 local time the clock jumps to 03:00. The 02:00-02:59 hour does not exist in local time.
Tests against 2026-03-08 02:30 America/New_York produce
ambiguous or invalid results depending on library:
| Library | Behaviour at non-existent local time |
|---|---|
Python pytz (legacy) | pytz.exceptions.NonExistentTimeError |
Python zoneinfo (3.9+) | Returns the "would-be" time + 1h (=03:30 EDT) |
Java ZonedDateTime | Constructor takes a resolver: STRICT / SMART_BACKWARD / SMART_FORWARD |
| JS Intl | Browsers vary; often returns the post-transition time |
In US Eastern: 1st Sunday of November at 02:00 local time, the clock falls back to 01:00. The 01:00-01:59 hour occurs twice - once as EDT (UTC-4), once as EST (UTC-5).
2026-11-01 01:30 America/New_York is ambiguous. Libraries
either:
is_dst / fold flag (Python 3.6+ has fold=0|1)Goal: prove the code under test handles a non-existent local time deterministically.
2026-03-08 02:30 America/New_York in the code path.zoneinfo normalises to 03:30 EDT - assert the
normalised value, never 02:30.pytz raises NonExistentTimeError - assert the raise.ZonedDateTime applies its resolver - assert per the
chosen STRICT / SMART_FORWARD rule.2026-11-01 01:30 America/New_York occurs
twice; assert the fold / is_dst selection picks the intended
offset.Per-region DST rules (US, EU, Australia, and the growing list of regions that abolished DST) and refreshable 2026 fixture timestamps live in references/jurisdictions-and-fixtures.md. Per IANA, rules change frequently - test against current zoneinfo, not assumptions.
A "daily at 02:30" cron in America/New_York:
Mitigation:
cron-job-test-author (in the qa-async-jobs plugin):
always test DST + leap-day edge cases."Bill on the 1st of each month at 00:00 local time":
Mitigation: bill at UTC, or at a local hour known to be safe (e.g., 06:00).
tomorrow_same_time = today_same_time + Duration("24 hours"):
Mitigation: distinguish "24 hours from now" (Duration) from "this time tomorrow" (calendar addition).
"Every Monday at 09:00 local time":
Storing wall-clock-local strings ("2026-03-08 02:30") is unsafe across DST. Always store UTC + zone identifier.
| Behaviour | Test |
|---|---|
| Code handles non-existent local time | Construct 2026-03-08 02:30 America/New_York; library raises or normalises; assert expected |
| Code handles ambiguous local time | Construct 2026-11-01 01:30 America/New_York; library raises or picks; assert |
| Cron-equivalent fires 0 / 1 / 2 times | Simulate clock across the transition; count invocations |
| Duration vs calendar addition consistent | Assert difference on transition day |
| Storage uses UTC + zone | Parse stored value; expect ISO format with offset or Z |
Per
timezone-test-matrix-builder,
the test matrix combines (zone, transition-type, library-version).
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Storing local times as strings | Ambiguous on fall-back; nonexistent on spring-forward | UTC + zone, or RFC 3339 with explicit offset |
| Assuming all jurisdictions observe DST | Half the world doesn't | Per-zone testing |
| Using "24 hours" for "tomorrow" | Off by 1 hour on transition days | Calendar arithmetic primitives |
| Pinning to a specific year's transition date | Rules change annually | Use IANA zoneinfo dynamically |
Crossing DST with naive datetime | Behaviour undefined | Always tz-aware |
| Cron in local time without DST testing | Misses / duplicates jobs | Test transition days |
| Hardcoded UTC offset (-5:00) | Wrong when DST is in effect | Use zone identifier |
zoneinfo reads system tzdata. Mismatches cause subtle bugs.leap-second-reference.leap-second-reference,
iso-8601-vs-rfc-3339-reference.cron-job-test-author (qa-async-jobs).libfaketime-c,
sinon-fake-timers-js,
jest-fake-timers,
freezegun-python,
timecop-ruby,
mockclock-jvm,
timezone-test-matrix-builder.