Testland
Browse all skills & agents

dst-transition-reference

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, refreshable per-region test-data fixtures, and the leap-second reference (23:59:60 insertion, time_t stalls, leap-smear vs step, monotonic-clock fixes) live in references/. Use when designing or auditing time-handling code or test cases, or when auditing leap-second assumptions.

Install with skills.sh (any agent)

npx skills add testland/qa --skill dst-transition-reference
View source

dst-transition-reference

Overview

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 (opens in new window)) is the canonical source of historical and current DST rules.

How to use this reference

  1. Identify the transition the code crosses - spring-forward (skipped hour) or fall-back (repeated hour) - from the DST mechanics section below.
  2. Match the bug class the code is exposed to (cron, billing, duration arithmetic, recurring meeting, storage) and apply its mitigation.
  3. Turn it into an assertion using the testable-behaviours table - construct the transition timestamp and assert the library's documented result (see the worked example).
  4. Pick zones + fixture timestamps from references/jurisdictions-and-fixtures.md, then refresh them against IANA before each release.

When to use

  • Designing time-handling code that crosses DST boundaries.
  • Auditing existing code for DST-safety.
  • Writing test cases that exercise DST behaviour.
  • Investigating "scheduled job ran twice / didn't run" reports.

DST mechanics

Spring-forward (skipped hour)

Per en.wikipedia.org/wiki/Daylight_saving_time (opens in new window), 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:

LibraryBehaviour 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 ZonedDateTimeConstructor takes a resolver: STRICT / SMART_BACKWARD / SMART_FORWARD
JS IntlBrowsers vary; often returns the post-transition time

Fall-back (repeated hour)

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:

  • Pick one (typically the first occurrence in pytz/zoneinfo)
  • Raise an error
  • Take an is_dst / fold flag (Python 3.6+ has fold=0|1)

Worked example - a spring-forward assertion

Goal: prove the code under test handles a non-existent local time deterministically.

  1. Pick the transition: America/New_York spring-forward on 2026-03-08 - 02:00 local jumps to 03:00, so 02:00-02:59 does not exist.
  2. Construct 2026-03-08 02:30 America/New_York in the code path.
  3. Assert against the library's documented behaviour (from the table above):
    • Python zoneinfo normalises to 03:30 EDT - assert the normalised value, never 02:30.
    • Python pytz raises NonExistentTimeError - assert the raise.
    • Java ZonedDateTime applies its resolver - assert per the chosen STRICT / SMART_FORWARD rule.
  4. Repeat for fall-back: 2026-11-01 01:30 America/New_York occurs twice; assert the fold / is_dst selection picks the intended offset.

Per-jurisdiction differences

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.

Common bug classes

Cron jobs

A "daily at 02:30" cron in America/New_York:

  • Spring-forward day: doesn't fire (02:30 doesn't exist).
  • Fall-back day: fires twice (02:30 EDT, then 02:30 EST).

Mitigation:

  • Use UTC cron expressions when possible.
  • For local-time business hours, accept the irregularity or schedule outside transition hours (04:00 is safe everywhere).
  • Per cron-job-test-author (in the qa-async-jobs plugin): always test DST + leap-day edge cases.

Billing periods

"Bill on the 1st of each month at 00:00 local time":

  • Fine in jurisdictions without DST.
  • Risk in DST-observing: 00:00 local on Nov 1 (US) might overlap with the fall-back hour if billing involves more than one event.

Mitigation: bill at UTC, or at a local hour known to be safe (e.g., 06:00).

Duration arithmetic

tomorrow_same_time = today_same_time + Duration("24 hours"):

  • Spring-forward: result is 23 hours later in local time.
  • Fall-back: result is 25 hours later in local time.

Mitigation: distinguish "24 hours from now" (Duration) from "this time tomorrow" (calendar addition).

Recurring meeting

"Every Monday at 09:00 local time":

  • Crosses DST boundary → still 09:00 local, but 2 minutes before or after the UTC equivalent of the previous week.
  • Calendar systems handle this; custom scheduling code often doesn't.

Storage

Storing wall-clock-local strings ("2026-03-08 02:30") is unsafe across DST. Always store UTC + zone identifier.

Testable behaviours

BehaviourTest
Code handles non-existent local timeConstruct 2026-03-08 02:30 America/New_York; library raises or normalises; assert expected
Code handles ambiguous local timeConstruct 2026-11-01 01:30 America/New_York; library raises or picks; assert
Cron-equivalent fires 0 / 1 / 2 timesSimulate clock across the transition; count invocations
Duration vs calendar addition consistentAssert difference on transition day
Storage uses UTC + zoneParse stored value; expect ISO format with offset or Z

Per timezone-test-matrix-builder, the test matrix combines (zone, transition-type, library-version).

Anti-patterns

Anti-patternWhy it failsFix
Storing local times as stringsAmbiguous on fall-back; nonexistent on spring-forwardUTC + zone, or RFC 3339 with explicit offset
Assuming all jurisdictions observe DSTHalf the world doesn'tPer-zone testing
Using "24 hours" for "tomorrow"Off by 1 hour on transition daysCalendar arithmetic primitives
Pinning to a specific year's transition dateRules change annuallyUse IANA zoneinfo dynamically
Crossing DST with naive datetimeBehaviour undefinedAlways tz-aware
Cron in local time without DST testingMisses / duplicates jobsTest transition days
Hardcoded UTC offset (-5:00)Wrong when DST is in effectUse zone identifier

Limitations

  • IANA zoneinfo changes throughout the year. Sept 2026 may add or remove DST observance for some jurisdictions; test data goes stale.
  • OS / runtime zoneinfo versions differ. Java's tzdata ships with the JDK; system tzdata is separate; Python's zoneinfo reads system tzdata. Mismatches cause subtle bugs.
  • Polar regions, antimeridian, and historic timezones. Special cases not covered here.
  • Leap seconds are a separate discontinuity class. The 23:59:60 insertion mechanics, bug classes, and monotonic-clock fixes are in references/leap-seconds.md.

References

Per-jurisdiction DST rules and test-data fixtures

View source (opens in new window)

Per-jurisdiction DST rules and test-data fixtures

Deep reference for dst-transition-reference SKILL.md. Consult when choosing which zones to cover and for refreshable per-region fixture timestamps.

Per-jurisdiction differences

RegionDST behaviour
US (most)Spring-forward 2nd Sun March; fall-back 1st Sun November
EULast Sun March; last Sun October (one hour earlier)
Australia (most of NSW/VIC)First Sun October; first Sun April (Southern hemisphere - reversed)
Australia (QLD, NT, WA, NT)No DST
Japan, China, IndiaNo DST
RussiaAbolished DST in 2011
IranAbolished DST in 2022
MexicoAbolished mainland DST in 2022
BrazilAbolished DST in 2019

Per IANA: rules change frequently. Test against current zoneinfo, not assumptions.

Test data fixtures

Useful canonical timestamps per region (refresh against IANA):

RegionSpring-forward 2026Fall-back 2026
America/New_York2026-03-08 02:00 → 03:00 EDT2026-11-01 02:00 → 01:00 EST
Europe/London2026-03-29 01:00 → 02:00 BST2026-10-25 02:00 → 01:00 GMT
Australia/Sydney2026-10-04 02:00 → 03:00 AEDT2026-04-05 03:00 → 02:00 AEST

These dates change year to year (some); commit a current fixture and refresh annually.

Leap-second mechanics and bug classes

View source (opens in new window)

Leap-second mechanics and bug classes

A leap second is an extra second (23:59:60 UTC) inserted to keep UTC within 0.9s of UT1. Per IERS Bulletin C (datacenter.iers.org/data/latestVersion/bulletinC.txt (opens in new window)), insertions get ~6 months of notice. 27 were inserted 1972-2016 (most recent 2016-12-31); none since, and per the 27th CGPM resolution (2022) leap seconds will be abolished by 2035. Audit this surface only when second-granular progress matters: financial timestamping, distributed logs, NTP-sensitive schedulers.

Mechanics

PropertyDetail
FrequencyIrregular; announced by IERS Bulletin C
Insertion pointLast second of UTC June 30 or December 31
Wire format23:59:60 UTC (a real 61st second of the minute)
POSIX time_tDoes NOT include leap seconds; it stalls or jumps back 1s on insertion
NTPCarries a leap indicator; client handling varies (2012 Linux kernel-hang incident)

Whether a host inserts a real 23:59:60 or smears it over 24 hours (Google / AWS) is per-host - see smear-strategies-and-history.md (opens in new window). A smear is invisible to applications; a real insertion exposes the discontinuities below.

Bug classes

  1. time_t non-monotonicity - on insertion the clock stalls (1483228799 repeats) or jumps; "1s of CPU = 1s of clock" breaks.
  2. Negative durations - time.time() - start can go below zero across the insertion. Fix: monotonic clocks (time.monotonic(), clock_gettime(CLOCK_MONOTONIC)) for all duration measurement.
  3. NTP cascading - OS-dependent handling of the leap indicator.
  4. Distributed clock skew - one node steps, another smears; skew temporarily exceeds 1s. Per AWS (opens in new window), AWS smears specifically to avoid this.

Testable behaviours

BehaviourTest
Durations use a monotonic clocktime.monotonic() deltas stay >= 0 across the leap
Sortable timestamps don't collideSequence numbers / sub-second resolution beside stalled time_t
Cron at 00:00:00 UTC of leap dayFires exactly once
Per-host absorption strategy knownVerify step vs smear per host before comparing cross-node timestamps

Worked assertion - negative durations

start = time.monotonic()
do_work()                     # crosses 2016-12-31 23:59:60 UTC
elapsed = time.monotonic() - start
assert elapsed >= 0           # holds; the time.time() form can trip

Simulate by pinning a fake clock to the last real leap second (freeze_time('2016-12-31 23:59:59 UTC') - see the fake-clock-testing skill) and advancing across it. Caveat: fake clocks can't replay the OS-level leap indication - this asserts the code's clock choice, not the kernel's behaviour; a real leap needs an OS-level test.

Anti-patterns

Anti-patternWhy it failsFix
time.time() - start for durationsWall clock; affected by leaptime.monotonic()
Treating time_t as continuousHistorical insertions broke itPer IANA leap-seconds.list (opens in new window)
Hardcoding 86400 seconds-per-dayOnly sometimes trueCalendar arithmetic
Assuming all servers smearSome stepVerify per-host strategy

References

Leap-smear strategies and the historical leap-second record

View source (opens in new window)

Leap-smear strategies and the historical leap-second record

Deep reference for leap-seconds.md (opens in new window). Consult when comparing how platforms absorb a leap second and when you need the factual insertion history.

Leap-smear

Per Google's "Time, technology and leaping seconds": googleblog.blogspot.com/2011/09/time-technology-and-leaping-seconds.html (opens in new window), Google "smears" the leap second rather than stepping the clock. The published standard is a "24-hour linear smear from noon to noon UTC" (Google Public NTP: Leap Smear (opens in new window)), adding a small fraction to each second so the total adds up to 1 second of slowdown, with no actual 23:59:60.

Leap second strategy comparison:

| Approach              | What happens                       |
|-----------------------|------------------------------------|
| IERS spec             | 23:59:60 UTC inserted (real second)|
| Linux kernel default  | Real insertion; time_t stalls 1s  |
| Google leap-smear     | Distributed over 24h               |
| AWS leap-smear        | Linear over 24h                    |
| NTP "step"            | Jump 1s; subsequent time_t differs |

The smear is operationally invisible to applications; the spec exposes the discontinuity.

Historical leap seconds

Per IERS, 27 leap seconds were inserted between 1972 and 2026. Most recent: 2016-12-31 23:59:60 UTC. None have been added since: IERS Bulletin C 72 (6 July 2026) states "from 2017 January 1, 0h UTC, until further notice : UTC-TAI = -37 s" and that "NO leap second will be introduced at the end of December 2026" (datacenter.iers.org, Bulletin C (opens in new window)). None expected before 2035 abolition.

Related skills

fake-clock-testing

Fake clocks / freeze time in tests across every mainstream runtime: freezegun (Python), Jest fake timers + Sinon @sinonjs/fake-timers (JS/TS), timecop (Ruby), java.time.Clock / InstantSource injection (JVM), .NET TimeProvider / FakeTimeProvider, and libfaketime (LD_PRELOAD for any native binary). Covers the language-agnostic discipline - inject or patch the clock, freeze vs tick vs advance vs set-system-time semantics, teardown so fake clocks never leak between tests - plus the shared anti-pattern table (real sleep under a frozen clock, leaked clock state, timezone-dependent assertions). Per-library setup, API, and CI recipes live in references/{python,js,ruby,jvm,dotnet,libfaketime}.md. Use when tests need deterministic control of now(), timers, or timeouts in any language, or when choosing the right fake-clock tool for a stack.

timezone-test-matrix-builder

Builds a timezone, daylight saving time (DST), and leap year / leap second test matrix from wherever a codebase reads or formats dates and times. Finds time-handling code (grep for datetime / Date / Instant / time.time / timezone), sorts each spot into storage, business-logic, display, cron, or billing, picks the edge cases that matter (DST spring-forward / fall-back, ambiguous local time, leap day Feb 29, ISO 8601 / RFC 3339 round-trip, zone-database updates), and emits per-spot test stubs wired to the language's fake-clock (mock-time) library. Use when a codebase needs timezone, DST, and leap-year test coverage derived from its own date/time usage.