By Ether DataRequest data sample
All sections

Requested vs. Executed Time

A local wall time is what was requested; the UTC slot is what was executed; a DST gap or fold means the two cannot both hold exactly, and the record must say so rather than silently pick one.

stable4 min read
Source time
local_datetime, iana_zone
Destination time
instant, hour_of_week_slot

The geo-interoperability KB draws a hard line between what a buyer requested (a polygon, a radius, an address) and what a system executed (a set of H3 cells, a circumscribed circle). This page ports that doctrine to time. A local wall time — "8:00 PM in Chicago" — is a request. The canonical UTC slot it resolves to is the execution. Most of the time the two agree so cleanly that the distinction feels academic. Twice a year, for one hour each, they cannot both be honored exactly, and that is precisely when a system's honesty is tested.

Why they can diverge

Daylight saving time transitions create two structurally different failure modes, both handled by localToSlot and surfaced end-to-end by resolveLocalToCanonical in lib/time/:

Spring-forward gap. At the DST-start transition, clocks jump forward an hour and a whole range of wall times simply never occurs. America/New_York skips from 01:59:59 directly to 03:00:00 on 2026-03-08, so 02:30 that morning is not a request that can be executed as-written — it names a moment that does not exist on that clock. There is exactly one adjacent valid instant (03:30 EDT, the same clock position measured forward), and the system's only honest choices are: resolve forward to it, resolve backward to the pre-gap instant, or reject the request outright.

Fall-back fold. At the DST-end transition, clocks repeat an hour, so a wall time names two different instants. America/New_York repeats 01:00–01:59 on 2026-11-01, once at UTC-4 (EDT) and once at UTC-5 (EST). 01:30 that morning is ambiguous between two UTC instants an hour apart — and, because slots are hour granular, potentially two different slots.

Never silently resolve a gap or fold

A system that picks a resolution for a gap or fold without a declared policy, and without recording that it did so, has quietly converted an ambiguous or invalid request into a false-precision answer. The record must carry wasNonexistent / wasAmbiguous and the policy applied — never just the resolved instant.

The policy parameter

Both hazards are resolved by a declared disambiguation policy, not a default buried in a library:

earliest
Fold: the first (pre-transition, larger-offset) occurrence. Gap: the single valid post-transition instant.
latest
Fold: the second (post-transition, smaller-offset) occurrence. Gap: the same single valid instant as earliest (there is only one).
reject
Throw rather than guess, for either hazard, when silent resolution is unacceptable.

Other zone examples clarify the two hazards further: 05:30 in Asia/Kolkata never touches a DST transition at all — India has observed a fixed +5:30 offset since 1945 — so it resolves unambiguously to 00:00 UTC, which is slot 0. A zone inferred from geography (see inferred-timezone-from-geo) inherits whichever policy its wall time requires only if the zone itself is correctly resolved first — a wrong zone produces a confidently wrong slot with no wasAmbiguous flag to catch it, because the ambiguity was in the zone lookup, not the clock arithmetic.

Worked example

import { resolveLocalToCanonical } from "@/lib/time/provenance";

// Fall-back fold: 2026-11-01 01:30 America/New_York occurs twice.
const earliest = resolveLocalToCanonical(
  { year: 2026, month: 11, day: 1, hour: 1, minute: 30 },
  "America/New_York",
  "earliest",
);
// earliest.executed.utc        -> "2026-11-01T05:30:00.000Z"
// earliest.executed.wasAmbiguous -> true
// earliest.executed.lossless   -> false

const latest = resolveLocalToCanonical(
  { year: 2026, month: 11, day: 1, hour: 1, minute: 30 },
  "America/New_York",
  "latest",
);
// latest.executed.utc          -> "2026-11-01T06:30:00.000Z"
// same requested wall time, one hour and one slot apart

// Spring-forward gap: 2026-03-08 02:30 America/New_York never occurs.
const gap = resolveLocalToCanonical(
  { year: 2026, month: 3, day: 8, hour: 2, minute: 30 },
  "America/New_York",
  "earliest",
);
// gap.executed.utc             -> "2026-03-08T07:30:00.000Z"
// gap.executed.wasNonexistent  -> true
// gap.executed.lossless        -> false

resolveLocalToCanonical returns a requested object (the wall time, zone, grain, and disambiguation policy exactly as asked) and an executed object (the resolved slot, week-slot key, UTC instant, offset, which disambiguation was actually applied, and a lossless boolean that is false whenever wasNonexistent or wasAmbiguous is true). No field is overwritten or dropped — a caller who only reads executed.slot gets a correct answer; a caller who needs to know whether that answer required a judgment call reads executed.lossless and provenance alongside it.

Never silently rolled up

The same discipline extends past DST into grain: if a caller asks for hour-of-week slot delivery and the system can only report at a coarser grain — day or week — that coarsening must be a declared, requested operation, never a silent default (no-silent-temporal-rollup). A report that says "daily" when the caller asked for "hourly" has the same shape of dishonesty as a slot resolved from a fold without recording which occurrence was chosen: both replace an exact answer to the question asked with an approximate answer to a different, unstated question. See Resolution and grain for the decision guide on choosing a grain up front, and Timestamp to slot for the full conversion this page's doctrine governs.

Edge cases affecting this page

The Temporal Interoperability ModelConceptual modelMeasurement SemanticsMeasurement semantics