Purpose
A daypart — "primetime," "morning drive," "overnight" — is a band of local clock hours on a set of weekdays. It is a scheduling and measurement concept defined the way audiences experience it: 8pm feels like primetime everywhere, regardless of what UTC hour that is. The canonical unit in this KB, however, is the UTC hour-of-week slot. Converting a daypart to slots therefore requires resolving each local hour through the tz database for a specific IANA zone and a specific ISO week, because the local-to-UTC offset shifts across DST — the same local daypart maps to a different UTC slot set in a spring week than in a fall week.
Source and destination
Source: a daypart definition (local hours + weekdays), an iana_zone, and
an iso_week. Destination: a slot_set of UTC hour-of-week slots.
Exactness: weighted
weighted. Sub-hour offsets (India, Sri Lanka, Newfoundland at half-hour offsets; Nepal, Chatham Islands at 45-minute offsets) and dayparts whose edges fall on the half hour (a "daytime" band running 9:30am-4:30pm local) both mean the local band does not always align to whole UTC hours. When it doesn't, a single local hour straddles two UTC slots, so the correct output is not a clean slot list but a weighted slot-set — the fraction of coverage each UTC slot receives — the direct temporal analog of the geo KB's weighted crosswalk for a polygon that straddles a cell boundary. Rounding to whole-slot membership by majority overlap is a documented, lossy simplification, not the default behavior.
Algorithm
import { STANDARD_DAYPARTS, daypartToUtcSlots, utcSlotToDaypart } from "@/lib/time/daypart";
const primetime = STANDARD_DAYPARTS.find((d) => d.id === "primetime")!;
// { id: "primetime", name: "Primetime (8p-11p)", hours: [20, 21, 22] }
// Expand to UTC slots for a specific zone and ISO week (DST-correct for that week).
const janSlots = daypartToUtcSlots(primetime, "America/New_York", 2026, 3);
// Early January: EST is UTC-5, so 20:00-23:00 local -> 01:00-04:00 UTC next day.
const julySlots = daypartToUtcSlots(primetime, "America/New_York", 2026, 29);
// Mid-July: EDT is UTC-4, so the SAME local daypart resolves to a different
// UTC slot set than in week 3 — a one-hour shift purely from DST.
// Inverse: which daypart does a given UTC slot fall into, for a zone + week?
const back = utcSlotToDaypart(julySlots.slots[0]!, "America/New_York", 2026, 29);
// -> { daypart: "primetime", localHour: 20, localWeekday: <Mon0 weekday> }
STANDARD_DAYPARTS is a conventional US broadcast scheme (overnight, morning,
daytime, early fringe, early news, prime access, primetime, late news) defined
purely in local hours. It is a labelled default, not a universal
standard — Nielsen, individual networks, and international broadcasters each
define their own bands, sometimes at 15-minute resolution. Treat it the same
way this KB treats a conversion profile: override the hours/days per market
rather than assuming the US scheme applies elsewhere.
Parameters
- daypart
- An id/name/hours definition with optional days. hours are local clock hours 0-23; days default to all seven (Monday0-Sunday6).
- zone
- IANA zone the local hours are resolved in.
- isoYear / isoWeek
- The specific ISO week to resolve against — required because the local-UTC offset depends on the DST calendar for that week, not a fixed constant.
Outputs
A DaypartSlots record: the daypart id, zone, ISO year/week, and the sorted,
deduplicated set of UTC hour-of-week slots the daypart occupies in that week.
Where a sub-hour offset or a non-hour-aligned band edge applies, treat that
set as coverage-weighted rather than binary membership — carry the overlap
fraction through to any downstream reporting rather than silently rounding.
Units and convention
Local hours are integers 0-23; UTC slots follow the KB-wide convention (slot 0 = Monday 00:00 UTC); zones are IANA identifiers; the tz engine is luxon.
DST and disambiguation behavior
daypartToUtcSlots resolves each (weekday, local hour) pair as a wall time in
the target ISO week via luxon, so it inherits the correct DST offset for that
specific week automatically. An hour skipped entirely by a spring-forward gap
(the local hour that never occurs) is silently excluded from the slot set
rather than raising — callers doing exact accounting (e.g. ad-slot inventory)
should independently check for gap weeks via DST Handling
if that hour mattered to them.
Quality and provenance
Every result should carry the zone, ISO week, and daypart definition used — the same daypart id resolves to a different slot set in different weeks, so the week is not optional context, it is part of the key.
Edge cases
Half-hour and 45-minute offset zones, and the general sub-hour band straddle case, are why the output is weighted rather than a clean partition. Southern-Hemisphere reversed DST means "primetime" in Sydney and New York shift in opposite calendar directions across the year — never compare local-experience dayparts across hemispheres by UTC slot alone; see Measurement Semantics for the general UTC-canonical-vs-local-experience tension. Extreme offset span means a daypart aggregated across many zones can produce a slot set that wraps the entire week.
Python parity
from zoneinfo import ZoneInfo
from datetime import datetime
def daypart_to_utc_slots(hours, days, zone: str, iso_year: int, iso_week: int) -> set[int]:
slots = set()
for day_mon0 in days:
for hour in hours:
try:
local = datetime.fromisocalendar(iso_year, iso_week, day_mon0 + 1).replace(
hour=hour, tzinfo=ZoneInfo(zone)
)
except ValueError:
continue # hour skipped by a spring-forward gap
utc = local.astimezone(ZoneInfo("UTC"))
weekday_mon0 = (utc.isoweekday() - 1)
slots.add(weekday_mon0 * 24 + utc.hour)
return slots
The tested reference implementation is the TypeScript in lib/time/daypart.ts;
this Python mirrors daypartToUtcSlots using datetime.fromisocalendar (3.9+)
and zoneinfo, catching the ValueError a gap produces the same way the TS
skips an invalid luxon DateTime.
