This is the inverse of Timestamp to slot: given a canonical (ISO week x slot) coordinate, recover the concrete UTC interval it names. Unlike the forward conversion, this direction carries no DST ambiguity at all — it is pure, deterministic arithmetic over a timeline that has no gaps or folds in it, because UTC itself does not observe daylight saving time.
Purpose
Convert a hour_of_week_slot (0–167) plus an ISO week (isoYear,
isoWeek) into the concrete utc_interval — a half-open [start, end)
range spanning exactly one UTC hour — that the slot names within that
specific week. This is the conversion that turns an abstract, repeating
coordinate back into a schedulable, queryable moment: "run this campaign
during 2026-W30-S069" only means something once it is resolved to
[2026-07-22T21:00:00Z, 2026-07-22T22:00:00Z).
Source and destination
- source
- hour_of_week_slot (0-167) + iso_week (isoYear, isoWeek)
- destination
- utc_interval: a half-open [start, end) UTC range, exactly one hour wide
- exactness
- exact — pure arithmetic, no zone resolution, no DST hazard
- params
- isoYear (number), isoWeek (1-53), slot (0-167, normalized if out of range)
- outputs
- startUtc (epoch ms / ISO string), endUtc (startUtc + 3,600,000 ms)
- units
- instants in epoch milliseconds; interval width is exactly one UTC hour (3,600,000 ms)
Algorithm
import { isoWeekStartUtc } from "@/lib/time/isoweek";
import { slotToInstant, normalizeSlot } from "@/lib/time/slot";
function slotToUtcWindow(
isoYear: number,
isoWeek: number,
slot: number,
): { startUtc: string; endUtc: string; startMs: number; endMs: number } {
const weekStartMs = isoWeekStartUtc(isoYear, isoWeek); // Monday 00:00 UTC
const startMs = slotToInstant(weekStartMs, normalizeSlot(slot));
const endMs = startMs + 3_600_000; // exactly one UTC hour, always
return {
startUtc: new Date(startMs).toISOString(),
endUtc: new Date(endMs).toISOString(),
startMs,
endMs,
};
}
// The inverse of the worked example on "The 168 axis":
const window = slotToUtcWindow(2026, 30, 69);
// window.startUtc -> "2026-07-22T21:00:00.000Z"
// window.endUtc -> "2026-07-22T22:00:00.000Z"
isoWeekStartUtc anchors the repeating slot to a specific week by
resolving Monday 00:00 UTC of that ISO week; slotToInstant then adds
slot * 3,600,000 milliseconds. Because both steps operate purely in UTC
— no zone lookup, no local calendar — the result is deterministic for
every valid (isoYear, isoWeek, slot) triple, and normalizing the slot
with normalizeSlot makes the function total over any integer input
rather than throwing on an out-of-range value.
No-silent-rollup
If a caller asks for the UTC window of a specific slot and the serving system can only resolve to a day or week boundary — for example, a reporting table that only stores daily rollups — the correct response is an explicit rejection or a renegotiated grain, never a silently widened window. A caller who asked for a one-hour window and received an undisclosed 24-hour window has been given an answer to a coarser question than the one asked, and nothing in the response shape reveals that a substitution occurred.
This mirrors the geo KB's no-silent-rollup rule for cell aggregation: just as a system must never quietly return an H3 R5 cell's centroid when an R8 point was requested, this conversion must never quietly return a day-level window when an hour-level slot was requested. See Resolution and grain for how to negotiate grain up front so this situation is rare rather than a runtime surprise.
Quality and edge cases
The conversion is exact to the millisecond for any valid input, with one qualification: leap seconds. UTC has inserted 27 leap seconds since 1972 (with insertions expected to be phased out by around 2035), making an occasional UTC day 86,401 seconds rather than 86,400; cloud providers that "smear" the leap second across a 24-hour window can disagree with strict UTC by up to roughly half a second during the smear. For hour-of-week bucketing this is immaterial — a half-second discrepancy never crosses an hour boundary — but a system performing sub-second joins against this window (aligning a video frame or a bid-request timestamp to the hour boundary, say) should declare its clock model (UTC, TAI, or a specific smear algorithm) explicitly rather than assume all "UTC" timestamps in a join are measured against the same clock.
No-silent-temporal-rollup is the
single most consequential edge case for this conversion precisely because
the conversion itself has no failure mode of its own — the arithmetic is
exact — so the entire risk surface sits in how the resulting window is
reported downstream. Pair every UTC window this conversion returns with
the ISO week and slot it was derived from, so a consumer can always
verify the window matches the grain it originally requested. See
The 168 axis for the slot definition this
conversion inverts, and ISO week and the week-slot key
for how isoWeekStartUtc anchors the week boundary this window is
computed relative to.
