Skip to content

Spans

  • The core functions, which every industry uses.

diffZoned measures in calendar units and returns a Duration. Profiling, tracing and telemetry want a raw number, and the span/ namespace gives one: spanMs, spanNs and spanWallClock.

import { spanMs, spanNs, spanWallClock } from "@northguild/gmt";
spanMs("2024-03-10T12:00:00Z", "2024-03-10T12:00:01Z");
// 1000
spanMs("2024-03-10T12:00:00Z", "2024-03-10T12:00:00.123456789Z");
// 123.456789 — fractional, like performance.now()
spanNs("2024-03-10T12:00:00.123456789Z", "2024-03-10T12:00:00.123456790Z");
// 1n

Both spanMs and spanNs are signed. spanMs(b, a) is exactly -spanMs(a, b). Both measure exact elapsed time.

That is not the same question as calendar distance, and conflating the two is the most common span bug there is. A wall-clock day containing a DST transition is 23 or 25 hours long (24.5 in Australia/Lord_Howe), so:

const start = "2024-03-09T12:00:00-05:00[America/New_York]";
const end = "2024-03-10T12:00:00-04:00[America/New_York]";
spanMs(start, end);
// 82800000 — 23 hours actually elapsed
spanWallClock(start, end, "hours");
// 24 — the clock face advanced a full day
spanWallClock(start, end, "days");
// 1

spanWallClock reads each endpoint’s own local wall clock. It reads it straight off the string, never via an instant, so DST disambiguation cannot distort it, and a local time that never occurred is measured as written.

The two endpoints need not share a zone. A flight leaving New York at 23:00 and landing in Berlin at 11:00 the next local day is 12 wall-clock hours and 7 elapsed hours. It truncates toward zero, and it is not a count of midnights crossed.

isValidSpan(start, end) answers “will these two produce a span?”. It is true exactly when spanMs and spanNs will return a value. It is symmetric, since a reversed pair is a negative span rather than an invalid one.

spanWallClock has a different input grammar and is covered by isValidZonedDateTime.

  • 0 and 0n are valid spans, so invalid input returns null from all three rather than the zero the rest of the library uses for numbers. It is null and not NaN because null is the sentinel every other number-returning function uses, and the only one that strictNullChecks forces a caller to handle. A NaN types as plain number and propagates silently through arithmetic.
  • spanMs returns null past Number.MAX_SAFE_INTEGER milliseconds. That is about ±285,000 years, which two instants at opposite ends of Temporal.Instant’s range exceed. A sub-millisecond fraction counts toward that ceiling, so what comes back is always a safe integer or below. Use spanNs there. Its result is a duration, not an instant, and can be twice the epoch-nanosecond range.
  • Leap seconds are not counted. UTC repeats a second rather than numbering a 61st one, so a span across one is a second short of the physical elapsed time. Against a smeared clock (Google, AWS, Meta) the error is up to a second spread over the smear window.