Skip to main content
Technology

Daylight Saving in Code - The Bugs and How to Avoid Them

Gaps and Overlaps - The Two DST Anomalies

DST transitions break time continuity twice a year. At spring forward (e.g., U.S. Eastern at 2:00 AM jumping to 3:00 AM), the times 2:00-2:59 simply do not exist (a gap). At fall back (2:00 AM falling to 1:00 AM), the times 1:00-1:59 occur twice (an overlap). These two anomalies generate a remarkably long list of software bugs.

If a user enters a time inside a gap (say, 2:30 AM EST on the spring transition day), the right behavior is not obvious. Reject it as a validation error? Round forward to 3:30 AM EDT? Round back to 1:30 AM EST? Each choice has merits. The risk is that many libraries silently choose one and surprise developers who expected another.

Duration Math - 24 Hours Later Is Not Tomorrow Same Time

On DST transition days, a calendar day is 23 hours or 25 hours long. "Add 24 hours to current time" and "give me the same time tomorrow" usually agree, but on transition days they differ. Adding 24 hours to 1 AM on a spring forward day yields 2 AM on the next day, while "the same time tomorrow" is 1 AM.

The distinction has real consequences for recurring schedules ("send email at 9 AM daily") and deadlines ("cancel within 24 hours"). For "the same wall time every day," add one calendar day; for exactly 24 hours of physical time, add 86400 seconds to the UTC timestamp. Designs should distinguish these intents explicitly rather than treating them as equivalent.

Recurring Events - The Calendar App Challenge

A recurring "Tuesday at 14:00" appointment should remain at 14:00 local time across DST transitions. The user's intent is local wall time, not a fixed UTC moment. So recurring events should be stored as a (time zone, local time) pair, with the actual UTC time computed dynamically for each occurrence.

RFC 5545 (iCalendar) solves this with DTSTART and RRULE. DTSTART specifies a time zone-aware local time, and RRULE describes the repetition pattern. Each occurrence's UTC time is calculated from the time zone's rules including DST. With this design, "14:00 local" stays 14:00 local across transitions, and you avoid the embarrassing "my recurring 9 AM Monday meeting moved to 10 AM in March" complaint.

Database Storage - TIMESTAMP vs TIMESTAMPTZ

PostgreSQL's TIMESTAMP (WITHOUT TIME ZONE) stores values verbatim without any time zone conversion. TIMESTAMPTZ (WITH TIME ZONE) converts inputs to UTC for storage and converts back to the session's time zone on read. To handle DST correctly, TIMESTAMPTZ is the right default.

If you also need to remember the original local time as the user expressed it (e.g., a user-booked appointment time), TIMESTAMPTZ alone loses that information at conversion. The pattern is to store TIMESTAMPTZ plus the original time zone name in a separate column, restoring the user's intent at display time. This preserves both the absolute moment and the user's locale-specific intent.

Testing for DST - Catch the Twice-a-Year Bugs

DST bugs surface only twice a year, so they easily slip through ordinary testing. Test code must explicitly include moments around the transitions: input of times inside the gap, resolution of duplicate times in the overlap, duration arithmetic across transition days, and recurring events spanning a transition. Without these specific cases, the bug surfaces in production.

Fixing the test environment's time zone is also important. If developers run on UTC+9 (no DST) but production runs on America/New_York (with DST), tests pass locally and fail in production. Either pass time zone as a parameter to the function under test, or set the TZ environment variable explicitly during test runs to remove environment dependence from results.

XB!LINE

Was this article helpful?

Related Articles