# 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](https://www.iana.org/time-zones) 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)[^sussman2012] and the index of
that genre (Deldycke 2016)[^deldycke2016] 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](https://quality.stereobooster.com/static-types.md) 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](https://nodatime.org/) for .NET, [jiff](https://github.com/BurntSushi/jiff) for Rust, and
[Temporal](https://tc39.es/proposal-temporal/docs/) 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](https://quality.stereobooster.com/linters.md) reject the value the type system
still admits, and confine clock access to one module. [Ruff](https://docs.astral.sh/ruff/)'s `DTZ`
rules (`flake8-datetimez`) flag a naive `datetime.now()`;
[Semgrep](https://semgrep.dev/) and an [ESLint](https://quality.stereobooster.com/eslint.md) `no-restricted-syntax` rule
forbid `new Date()` outside a designated clock module;
[forbidigo](https://github.com/ashanbrown/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)[^klyne2002].

## 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](https://quality.stereobooster.com/supply-chain-hygiene.md) 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](https://quality.stereobooster.com/property-based-testing.md) covers the space no hand-written
example list will: [Hypothesis](https://hypothesis.readthedocs.io/) draws real zones from the tz
database with `datetimes(timezones=timezones())`, and
[fast-check](https://fast-check.dev/) and [jqwik](https://jqwik.net/) have equivalents. The
oracle that pairs with it is a round trip, `parse(format(t, z)) == t`, or a
[metamorphic relation](https://quality.stereobooster.com/metamorphic-testing.md) 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](https://quality.stereobooster.com/fuzzing.md) 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](https://quality.stereobooster.com/differential-testing.md) 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](https://github.com/wolfcw/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](https://github.com/spulec/freezegun) and [time-machine](https://github.com/adamchainz/time-machine) for Python,
[Sinon's fake timers](https://github.com/sinonjs/fake-timers) for JavaScript (reached
through `vi.setSystemTime` in [Vitest](https://vitest.dev/)), [Timecop](https://github.com/travisjeffery/timecop)
for Ruby, an injected `Clock` in Java, [clockwork](https://github.com/jonboulle/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](https://quality.stereobooster.com/deterministic-simulation-testing.md) 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](https://quality.stereobooster.com/flaky-tests.md#provoking-flakiness-on-purpose). 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)[^luo2014].

## Referenced by

- [Deterministic simulation testing](https://quality.stereobooster.com/deterministic-simulation-testing.md) · Methods

## References

[^sussman2012]: Sussman, Noah. 2012. *[Falsehoods Programmers Believe About Time](https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time)*. Infinite Undo (blog). <https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time>.
[^deldycke2016]: Deldycke, Kevin. 2016. *[Awesome Falsehood](https://github.com/kdeldycke/awesome-falsehood)*. GitHub repository kdeldycke/awesome-falsehood. <https://github.com/kdeldycke/awesome-falsehood>.
[^klyne2002]: Klyne, Graham, and Chris Newman. 2002. *[Date and Time on the Internet: Timestamps](https://www.rfc-editor.org/rfc/rfc3339.txt)*. RFC 3339. Internet Engineering Task Force. <https://doi.org/10.17487/RFC3339>.
[^luo2014]: Luo, Qingzhou, Farah Hariri, Lamyaa Eloussi, and Darko Marinov. 2014. "[An Empirical Analysis of Flaky Tests](https://mir.cs.illinois.edu/lamyaa/publications/fse14.pdf)." *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>.
