Two temporal objects in this KB look alike and behave completely
differently. An hour-of-week slot (0–167) is a position in a repeating
weekly cycle: slot 45 is "Tuesday 21:00 UTC" in every week, forever. A
UTC window like [2026-11-01T05:00Z, 06:00Z) is a concrete interval:
it happens once. Confusing the two is the temporal version of mixing a cell
system with a cell id — the values type-check as numbers and strings, so
nothing complains until a set operation quietly returns the wrong answer.
Every conversion result in this KB therefore carries a temporal_kind
discriminator: cycle or interval.
The two kinds
- cycle
- A position in the repeating weekly cycle: an hour-of-week slot (0–167) or a daypart. Recurs every week; has no year. Answers 'when in a typical week.'
- interval
- A concrete dated span or instant: a UTC window, an epoch instant, an ISO (year × week) coordinate. Happens once. Answers 'which actual hour in history.'
A cycle value is incomplete on its own the way an H3 resolution is incomplete without a cell id: "slot 45" is not a moment until you pair it with an ISO week. An interval value is fully grounded — it names a specific hour that already has (or will have) happened.
Why the distinction is load-bearing
The two kinds support different operations, and mixing them silently corrupts the result:
- Set algebra only closes within one kind. You can intersect or subtract two intervals (concrete UTC spans) exactly. You can intersect two cycles (slot-sets) exactly. You cannot subtract a cycle from an interval without first binding the cycle to a specific week — the temporal analog of the geo rule that set operations must stay inside one cell system.
- Binding is where DST enters. Turning a cycle into an interval — "slot 2 of this week, in this zone" — is exactly where a 23- or 25-hour day, a skipped local hour, or a repeated local hour appears. A cycle has no DST; the interval it binds to does. See Requested vs executed time.
- Projecting the other way is lossy. Dropping the week from an interval to get "just the slot" is fine for an ordinary week and wrong for a DST-variant one, because the skipped/repeated local hour has no stable cyclic home. That is why coarsening is a declared step, never a default (no silent temporal rollup).
Joining a table keyed by hour-of-week slot (a cycle) to one keyed by a UTC timestamp (an interval) on the bare integer is the single most common temporal interoperability bug. The slot repeats every week; the timestamp does not. Bind the slot to each week — or aggregate the timestamp to a (week × slot) key — before the join. Never join a cycle to an interval on the raw number.
What every tool declares
The live conversion tools tag each result so a consumer never has to infer the kind from the field names:
- time_to_canonical
- cycle — the product is the hour-of-week slot; executed.weekSlotKey is its interval realization in one week
- time_instant_to_slot
- cycle — a bare slot 0–167
- time_daypart_to_slots
- cycle — a weekly slot-set
- time_slot_uncertainty
- cycle — candidate slots a ± window touches
- time_now
- interval — a concrete instant
- time_week_slot_key
- interval — a concrete (year × week × slot) key
- time_parse_week_slot_key
- interval — same, parsed back
- time_slot_to_utc_window
- interval — the concrete UTC hour a slot occupies in a stated week
- time_iso_week_of
- interval — a concrete ISO week
- time_broadcast_day_slots
- interval — a specific dated broadcast day (23/24/25 UTC hours)
- time_holidays_for
- interval — concrete dated holidays
The rule of thumb: if the result would be identical next week, it is a
cycle; if it names a specific year and week, it is an interval. The
time_slot_to_utc_window and time_to_canonical pair is the canonical
bridge between them — bind a cycle to a week to get an interval, and read an
interval's slot to get back the cycle.
