By Ether DataRequest data sample
All sections

Broadcast Day

A broadcast day is a local day that starts at a declared cutover, not at midnight, so hours before the cutover belong to the previous broadcast day.

stable5 min read
Source time
instant, iana_zone
Destination time
broadcast_day, slot_set
policy dependent

Purpose

A broadcast day is a local calendar day that does not start at local midnight. It runs from a declared cutover hour (the operational default in this KB is 06:00 local) on calendar date D to the same cutover on date D+1. Anything airing between midnight and the cutover is logged under the previous broadcast day's label — a program airing at 01:00 local on 2026-03-09 is reported as part of the 2026-03-08 broadcast day, not 03-09. This convention exists because overnight programming, sports that run past midnight, and ad-log reconciliation all need a single day boundary that does not fall in the middle of prime evening/overnight viewing. It is the calendar analog of choosing where to draw a boundary in geo: someone must declare it, and the declaration must travel with every result.

Source and destination

Source: an instant (UTC epoch) plus an IANA zone. Destination: a broadcast_day label (a local calendar date and its declared cutover) and the slot_set of UTC hour-of-week slots that day occupies.

Exactness: policy dependent

policy dependent because the result depends entirely on a parameter the caller must supply — the cutover hour — and because the number of UTC hours a broadcast day spans is not fixed: it varies with the local DST calendar for that specific date. There is no "just compute it" version of this conversion; the cutover is a business rule, not a physical constant, and it must be declared and echoed on every result (requested vs executed), exactly as the geo KB requires the requested and executed geometry to both be visible.

Algorithm

import {
  broadcastDayOf,
  broadcastDayInterval,
  broadcastDayToSlots,
  DEFAULT_CUTOVER_HOUR,
} from "@/lib/time/broadcast";

// Which broadcast day does this instant belong to, in this zone?
const day = broadcastDayOf(Date.parse("2026-03-09T05:30:00Z"), "America/New_York", 6);
// -> { broadcastDate: "2026-03-08", cutoverHour: 6, zone: "America/New_York" }
// A 00:30 local airing (before the 06:00 cutover) rolls back to the prior day.

// The true [start, end) UTC interval — calendar-aware, so it honours DST.
const spring = broadcastDayInterval({ broadcastDate: "2026-03-08", cutoverHour: 6, zone: "America/New_York" });
// utcHours: 23 — 2026-03-08 in America/New_York is the spring-forward day (clocks skip 02:00->03:00),
// so the 24 *local* hours from 06:00 to next-day 06:00 span only 23 UTC hours.

const fall = broadcastDayInterval({ broadcastDate: "2026-11-01", cutoverHour: 6, zone: "America/New_York" });
// utcHours: 25 — 2026-11-01 is the fall-back day; the 06:00-to-06:00 local window
// spans 25 UTC hours because 01:00-02:00 local occurs twice.

// The set of UTC hour-of-week slots the broadcast day touches (deduplicated).
const slots = broadcastDayToSlots({ broadcastDate: "2026-03-08", cutoverHour: 6, zone: "America/New_York" });
// slots.length === 23 for the spring day above; wrapsWeek is true only when the
// interval crosses Sun 23:00 UTC -> Mon 00:00 UTC.

An ordinary, non-DST broadcast day produces exactly 24 UTC hours and 24 distinct slots. The DST day is the interop hazard: a spring-forward broadcast day is 23 UTC hours (23 slots), and a fall-back broadcast day is 25 UTC hours — but because one UTC hour genuinely recurs, broadcastDayToSlots still returns a deduplicated slot set (each UTC hour-of-week slot appears once), so its length is 24, while broadcastDayInterval.utcHours correctly reports 25 elapsed hours. Consumers that need to distinguish "24 distinct slots" from "25 hours elapsed" must read both fields — collapsing them loses the DST signal.

Parameters

cutoverHour
Local hour (0-23) the broadcast day begins. Default: 6 (06:00 local). Must be declared explicitly for any non-default operation.
zone
IANA zone the cutover is evaluated in, e.g. America/New_York.
broadcastDate
The local calendar date (YYYY-MM-DD) the broadcast day is labelled by — this is the OUTPUT label, not necessarily the date of the instant.

Outputs

broadcastDayOf returns the broadcast-day label. broadcastDayInterval returns the true [startMs, endMs) UTC interval and its actual utcHours (23, 24, or 25). broadcastDayToSlots returns the deduplicated UTC hour-of-week slot set and a wrapsWeek flag for the rare case where the interval crosses the Sunday 23:00 UTC to Monday 00:00 UTC boundary.

Units and convention

Instants are epoch milliseconds; zones are IANA identifiers resolved through luxon (the IANA tz database); UTC hour-of-week slots follow the same convention as the rest of this KB — slot 0 = Monday 00:00 UTC.

DST and disambiguation behavior

broadcastDayInterval computes the end of the day with a calendar-aware "plus one day" operation (luxon .plus({ days: 1 })), which honours DST rather than adding a fixed 24 hours. This is the correct behavior for a broadcast day: a "day" here means one full local calendar day at the cutover hour, whatever its true UTC duration turns out to be. broadcastDayToSlots then steps hour by hour across that true interval and de-duplicates by UTC slot, so the returned slot count reflects distinct hour-of-week buckets touched, not raw elapsed hours.

Quality and provenance

Every broadcast-day result should echo the cutover hour and zone it was computed with — the requested parameters — alongside the executed broadcastDate, utcHours, and slot set. A result missing the cutover hour cannot be audited: a 1am airing logged under the wrong day is nearly always traceable to an undeclared or silently-changed cutover.

Edge cases

Broadcast-day cutover is not universal: some operations use 05:00, 02:00, or midnight, and sports/overnight feeds often differ from entertainment scheduling — never assume 06:00. Spring-forward gap and fall-back fold are exactly what produce the 23- and 25-hour broadcast days shown above. Half-hour offset zones (India, Sri Lanka, Newfoundland) mean the cutover itself may not land on a UTC hour boundary, so the resulting slot set can start mid-slot. Broadcast calendar month explains how broadcast days roll up into a broadcast month/quarter that does not align to the Gregorian calendar — see Week Systems.

Python parity

from datetime import date, timedelta
from zoneinfo import ZoneInfo

def broadcast_day_of(instant_utc, zone: str, cutover_hour: int = 6) -> date:
    local = instant_utc.astimezone(ZoneInfo(zone))
    return local.date() - timedelta(days=1) if local.hour < cutover_hour else local.date()

The tested reference implementation is the TypeScript in lib/time/broadcast.ts; this Python mirrors broadcastDayOf only — computing the true DST-aware [start, end) interval requires walking zoneinfo transition data the same way broadcastDayInterval walks luxon's, and is intentionally left to the TS implementation as the single source of truth.

Edge cases affecting this page

ISO WeekCalendar & weekThe 168 AxisConceptual modelDST HandlingTime systems & zones