Skip to content

Correct at the Edges

  • The core functions, which every industry uses.

This is the detailed evidence behind the summary on Why GMT?. Date bugs hide at the extremes: the last nanosecond of a day, the first and last instant a clock can name, a daylight-saving change nobody tests because it is hundreds of millennia away. GMT tests those edges directly, and each paragraph below links the tests that prove it.

The last nanosecond of a day stays on its day

Section titled “The last nanosecond of a day stays on its day”

A PlainTime has no date, so in Temporal adding one nanosecond to 23:59:59.999999999 wraps to 00:00:00. intervalAbutsTime and intervalXorAllTime compare the endpoints they are given and never step one nanosecond from them, so an interval ending on the last nanosecond of the day is never mistaken for one that starts at midnight.

Tests: intervalAbutsTime · intervalXorAllTime · halfOpenIntervals

The first and last instant give exact answers

Section titled “The first and last instant give exact answers”

Temporal represents instants from -271821-04-20T00:00:00Z to +275760-09-13T00:00:00Z, 108 days either side of the epoch (TC39 Temporal). Nothing can be added to the last one, and intervalAbuts* and intervalXorAll* for Date, DateTime, Utc and Zoned never compute a boundary past it. They return the exact answer for intervals that end there, in UTC and in New York. At the other end, the first month is partial: its start lies before the range, but its end does not. endOfDate("-271821-04-19", "month") returns "-271821-04-30", and endOfDateTime, endOfUtc, endOfZoned and endOfUnix return the matching day, week, month and year ends.

Tests: intervalAbutsUtc · intervalAbutsZoned · intervalXorAllUtc · intervalXorAllZoned · intervalAbutsDate · intervalXorAllDateTime · endOfDate · endOfUtc · endOfZoned · endOfUnix

Daylight-saving rules hold to the end of the range

Section titled “Daylight-saving rules hold to the end of the range”

The last instant falls on 12 September +275760 in Santiago, a few days after a daylight-saving change on 7 September. startOfZoned and floorToZone start that month at local midnight on the offset then in force, -04:00. getLocaleZonedStartOfWeek starts an en-US week at 01:00, because that Sunday’s midnight never happened (TC39 Temporal startOfDay, RFC 9557 time-zone annotations). startOfQuarterForZoned, startOfUnix, intervalCountZoned and intervalCountUnix agree at the same instant.

Tests: startOfZoned · floorToZone · getLocaleZonedStartOfWeek · startOfQuarterForZoned · startOfUnix · intervalCountZoned · intervalCountUnix

Zone conversions are exact at the very end of the representable range, too

Section titled “Zone conversions are exact at the very end of the representable range, too”

In zones ahead of UTC — Sydney and Kiritimati among them — GMT returns the spec-correct ZonedDateTime for a wall-clock value at or near the last representable instant, which the underlying Temporal polyfill release cannot yet parse that close to the edge (fixed on its main; see Helping fix Temporal). It still rejects a value one nanosecond past the maximum, and any offset the spec would reject. The same helper resolves America/Santiago’s last representable day, which opens with a daylight-saving change, to the instant the transition actually allows rather than to a midnight that was skipped. TC39 Temporal treats a ZonedDateTime as valid whenever its epoch nanoseconds lie within the Instant range, whatever its wall-clock fields read.

Tests: zonedWallClock · zonedWallClockOperations

Spans and totals are exact across the whole range

Section titled “Spans and totals are exact across the whole range”

spanNs returns the full distance from the first instant to the last as a bigint. sumIntervals totals covered time exactly past 253 nanoseconds, about 104 days, which is where a JavaScript number stops holding every nanosecond (ECMA-262 Number.MAX_SAFE_INTEGER). It stays exact up to the whole range, PT4800000000H, with hours as the largest unit, as Temporal.Instant.until allows.

Tests: spanNs · sumIntervals

The Unix interval functions that compare, cut or combine intervals reject a fractional epoch, or one past Number.MAX_SAFE_INTEGER, with their documented sentinel. Past that point 2 ** 53 + 1 === 2 ** 53, so consecutive epoch values can no longer be told apart. The functions stay exact on Number.MAX_SAFE_INTEGER itself.

Tests: intervalAbutsUnix · intervalXorUnix · the rest of unix/interval

Every interval function is half-open, [start, end): the start is inside and the end is not. This is the convention of SQL:2011’s application-time PERIOD, RFC 5545’s non-inclusive DTEND, and EWD831. Touching intervals share no instant, so they do not overlap or intersect. Merging joins them, and subtracting one from the other removes nothing. Take an interval apart with subtractIntervals and put it back with mergeIntervals and intersectIntervals, and not a nanosecond is lost. The positional functions in plain/, utc/, zoned/ and unix/ follow the same rule, and the instant-typed ones are property-tested against interval/ at every boundary; Interval Basics compares the two families.

Tests: intervalsOverlap · intersectIntervals · mergeIntervals · subtractIntervals · interval algebra round trip · positional UTC against interval/

A defect GMT finds is fixed in the same change, together with every function that shares its cause. It is never deferred, skipped or written up as a known issue. pnpm run validate enforces this with scripts/test-markers.mjs, which fails the build on any skipped, focused, to-do or expected-to-fail test, and on any known-defect note. None of the 56,006 tests is switched off.