Skip to content

Software Quality

Verifying time and date handling

A time-and-date bug is a program computing the wrong when. It shows up as a timestamp converted through the wrong zone, an interval measured across a daylight-saving transition, a recurring schedule that skips or repeats a day, an expiry that fires early. The class appears in two settings. In the product, the code names the wrong moment. In the test suite, a test that reads the ambient clock or zone passes on the machine that wrote it and fails on 29 February, at a UTC day boundary, or on a runner configured for another zone.

Two representations and the conversion between them

An instant is a point on the physical timeline: a UTC timestamp, a Unix epoch count, a monotonic reading. A civil time is a wall-clock reading in a place, such as 2024-03-31 02:30 in Berlin. The mapping between the two is data rather than arithmetic. The IANA time zone database records the offsets and daylight-saving rules each jurisdiction has enacted, and is revised several times a year.

Three properties of that mapping generate most of the bug class.

  • It is not total. A civil time inside a spring-forward gap names no instant; a civil time inside an autumn fall-back overlap names two. Code that assumes a bijection is wrong twice a year wherever daylight saving is observed.
  • It is not stable. A future meeting stored as an instant moves in local terms when the jurisdiction changes its rules; stored as a civil time plus a zone identifier, it stays where the participants expect. Neither representation is correct for every case: a past event is an instant, a future appointment is civil time.
  • Duration is not calendar arithmetic. Adding 24 hours and adding one day give different answers when a transition falls between; adding one month to 31 January has no arithmetic answer at all, only a convention.

What an offset can be

The range of legal offsets runs from −12:00 to +14:00, at quarter-hour rather than hourly granularity. A daylight-saving shift is not always an hour, and not always in the same month. Code written against the northern-hemisphere, whole-hour, positive-offset case fails on data that has been in the tz database for decades. A zone abbreviation is not an identifier: IST is Indian, Irish, and Israeli time, and CST is used on three continents, which is why the tz database is keyed by Area/Location and a string like EST cannot be resolved to an offset. A calendar date is not guaranteed to exist in a given place. The falsehoods programmers believe about time list (Sussman 2012)1 and the index of that genre (Deldycke 2016)2 catalog the same assumptions at greater length. Both are crowd-sourced enumerations of boundary cases and their refutations, with no prevalence data behind them, so they supply test input rather than support for a claim.

A small set of zones stresses these properties, and a suite that only ever runs under UTC exercises none of them.

Zone Property
Pacific/Chatham 45-minute offset, with daylight saving
Asia/Kolkata 30-minute offset, no daylight saving
Asia/Kathmandu 45-minute offset, no daylight saving
Australia/Lord_Howe 30-minute daylight-saving shift rather than an hour
Pacific/Kiritimati +14:00, the maximum offset
America/Santiago southern-hemisphere daylight saving, so transitions invert
Pacific/Apia 30 December 2011 does not exist (date-line change)

Removing the class by construction

A single ambiguous timestamp type is the root defect: when one type means both an instant and a civil reading, every conversion is implicit and none is checked. Static types that separate the two and demand a zone at the conversion make the ambiguous value unrepresentable, in the same way an option type makes an unchecked null unrepresentable. Java's Instant / LocalDateTime / ZonedDateTime split does this, as do Noda Time for .NET, jiff for Rust, and Temporal for JavaScript. Python's datetime keeps tzinfo optional, so a zone-less (naive) value stays representable and the check falls back to a linter.

Linters reject the value the type system still admits, and confine clock access to one module. Ruff's DTZ rules (flake8-datetimez) flag a naive datetime.now(); Semgrep and an ESLint no-restricted-syntax rule forbid new Date() outside a designated clock module; forbidigo forbids time.Now in Go packages other than the one that wraps it.

Making the ambiguous value unrepresentable applies at the storage and interchange boundaries too. A Postgres timestamptz column stores an instant and converts on read; a timestamp column stores a civil reading whose zone is whatever the writer assumed and never recorded. On the wire, RFC 3339 makes the offset mandatory, so a conforming timestamp admits no ambiguous reading (Klyne and Newman 2002)3.

Keeping the zone data current

The tz database changes when a legislature changes the rules, sometimes with a few weeks' notice, so its release cadence is set by politics rather than by software. A deployment running a stale copy produces wrong civil times for the affected zone from the moment the rule takes effect, with no error and no crash, which makes staleness a correctness defect rather than the maintenance issue supply-chain hygiene usually treats it as.

A single stack normally carries several independent copies: the operating system's /usr/share/zoneinfo, the JVM's bundled data, ICU's copy behind the browser and the formatting libraries, and the database server's own. They update on different schedules, so two components in one request path can disagree about the same zone. Pinning the version explicitly and refreshing it on the cadence used for security updates keeps the copies from drifting apart silently.

Methods that target the class, and the corpus they need

Most of the method catalog reaches time and date bugs only when someone points it at them, and what points it is the input corpus, not the method.

Generated inputs drawn from the zone data. Property-based testing covers the space no hand-written example list will: Hypothesis draws real zones from the tz database with datetimes(timezones=timezones()), and fast-check and jqwik have equivalents. The oracle that pairs with it is a round trip, parse(format(t, z)) == t, or a metamorphic relation that names neither result: converting to a zone and back is the identity wherever the mapping is total, and the duration between two instants does not depend on the display zone. The relation can also be an inequality — adding 24 hours and adding one day must differ across a transition, and a test asserting they agree encodes the bug.

Fuzzing a parser. A date parser accepts untrusted text with a large grammar, which is the standard fuzzing target shape; crashes and hangs on malformed offsets, absurd years, and truncated fractional seconds come out of a short campaign.

Differencing against an independent implementation. Differential testing runs the same inputs through a second reader of the same zone data: the platform's zoneinfo, another language's library, or the previous release of the same library after a database bump. This is the check that catches an enacted rule change breaking stored future timestamps, and it is the one method here with no counterpart among the by-construction fixes.

The corpus these mechanisms consume is enumerable and already published, so it can be generated rather than guessed. zdump -v -c 1970,2040 Europe/Berlin prints every offset transition the tz database records for a zone; each one yields instants one second before, at, and one second after, plus the civil times the transition renders non-existent or ambiguous. Running that over the zone list converts the database into a boundary-value corpus that refreshes whenever the database does. Cases the database does not supply have to be listed: 29 February and the non-leap century year, an ISO week-53 year, 31 January plus one month, the 32-bit time_t rollover in 2038, and the leap second (the most recent inserted at the end of 2016).

Controlling the clock

Reaching the corpus requires running code at an instant other than now. libfaketime intercepts the C library's time calls, so a whole process moves with no change to the code under test, which is what makes it usable on dependencies. In-process substitutes are narrower and faster: freezegun and time-machine for Python, Sinon's fake timers for JavaScript (reached through vi.setSystemTime in Vitest), Timecop for Ruby, an injected Clock in Java, clockwork for Go. Where a failure depends on when a clock change interleaves with other events, the clock has to be replaced along with the scheduler and the network, which is deterministic simulation testing rather than a fake timer.

Finding time-dependent tests

Varying the same inputs against the suite itself finds the tests that depend on the clock — the general form is provoking flakiness. Running the suite twice, once under a shifted clock and a rotated TZ, gives two pass/fail sets whose difference is the list of time-coupled tests. Rotating the value per CI run and logging it finds the same tests over a few weeks at a fraction of the cost of a full matrix. Clock-read confinement prevents the recurrence, because a test whose code cannot reach the system clock cannot depend on it by accident.

Evidence

No controlled study measures the incidence of date and time defects in production code, or the fault-detection rate of any method against them; the case for the methods here rests on the definitional point that the class comes from unmarked conversion and undeclared inputs. The adjacent measurement is flakiness. Of 161 flaky-test fixes classified by root cause, time accounted for 5; async waits, concurrency, and test-order dependency ranked well ahead of it. The mechanism is a test reading the system clock and failing when the date rolls over in UTC (Luo et al. 2014)4.

Referenced by

References


  1. Sussman, Noah. 2012. Falsehoods Programmers Believe About Time. Infinite Undo (blog). https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time

  2. Deldycke, Kevin. 2016. Awesome Falsehood. GitHub repository kdeldycke/awesome-falsehood. https://github.com/kdeldycke/awesome-falsehood

  3. Klyne, Graham, and Chris Newman. 2002. Date and Time on the Internet: Timestamps. RFC 3339. Internet Engineering Task Force. https://doi.org/10.17487/RFC3339

  4. Luo, Qingzhou, Farah Hariri, Lamyaa Eloussi, and Darko Marinov. 2014. "An Empirical Analysis of Flaky Tests." Proceedings of the 22nd ACM SIGSOFT International Symposium on Foundations of Software Engineering (FSE '14) (Hong Kong, China), 643–53. https://doi.org/10.1145/2635868.2635920