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¶
- Deterministic simulation testing · Methods
References¶
-
Sussman, Noah. 2012. Falsehoods Programmers Believe About Time. Infinite Undo (blog). https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time. ↩
-
Deldycke, Kevin. 2016. Awesome Falsehood. GitHub repository kdeldycke/awesome-falsehood. https://github.com/kdeldycke/awesome-falsehood. ↩
-
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. ↩
-
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. ↩