Files
skills/docs/foundation/timing/cron/src/timezone/timezone.md
2026-05-29 08:20:56 +01:00

3.5 KiB

timezone

(no submodules)

Purpose

The timezone module converts between epoch-millisecond timestamps and the wall-clock components observed in a specific IANA timezone using only the platform-native Intl.DateTimeFormat — no external timezone database is shipped or required. It powers the cron next-run algorithm by translating the user's "every day at 02:00 New York time" into the actual moments when that wall-clock instant occurs, including the awkward edge cases of daylight-saving-time spring-forward gaps and fall-back overlaps.

Features

  • toWallClock(epochMs, tz) — decompose an epoch instant into year / month / day / hour / minute / second / weekday in the requested zone.
  • fromWallClock(components, tz) — the inverse; performs up to four bounded fixed-point iterations to converge on the correct epoch even across DST transitions.
  • detectDstTransition(before, after, tz) — classifies a transition between two adjacent instants as "spring-forward", "fall-back", or null when no transition is crossed.
  • assertTimezone(tz) — eagerly validates an IANA name so the caller can fail fast at the API boundary.
  • Per-zone formatter cache keeps Intl construction off the hot path.

Usage: Basic Example

import { toWallClock, fromWallClock } from "@suaveplan/cron";

const wall = toWallClock(Date.UTC(2026, 0, 1, 5, 0, 0), "America/New_York");
console.log(wall.hour); // 0 — midnight in NY

const back = fromWallClock(
    { year: 2026, month: 1, day: 1, hour: 0, minute: 0, second: 0 },
    "America/New_York"
);
console.log(back === Date.UTC(2026, 0, 1, 5, 0, 0)); // true

Usage: Advanced Example

import {
    detectDstTransition,
    fromWallClock,
    toWallClock,
} from "@suaveplan/cron";

const start = Date.UTC(2026, 2, 8, 6, 0, 0);
const end = Date.UTC(2026, 2, 8, 8, 0, 0);
const transition = detectDstTransition(start, end, "America/New_York");
console.log(transition); // "spring-forward"

const requested = { year: 2026, month: 3, day: 8, hour: 2, minute: 30, second: 0 };
const real = fromWallClock(requested, "America/New_York");
const observed = toWallClock(real, "America/New_York");
console.log(observed.hour, observed.minute); // not 02:30 — the slot was skipped

API Reference

  • interface WallClock — { year, month, day, hour, minute, second, weekday } with weekday ∈ 0..6 (Sunday = 0, matching cron numbering).
  • toWallClock(epochMs: number, tz: string): WallClock.
  • fromWallClock(components: Omit<WallClock, "weekday">, tz: string): number.
  • detectDstTransition(beforeEpochMs, afterEpochMs, tz): "spring-forward" | "fall-back" | null.
  • assertTimezone(tz: string): void — throws CronError(CRON.TIMEZONE_INVALID) for invalid IANA names.

Implementation Notes

Intl.DateTimeFormat is the only timezone authority. The package never ships a tz database — keeping it universal and dependency-free. The inverse fromWallClock works by guessing UTC, observing the wall clock offset that produces, and applying it back. Two iterations suffice for every IANA zone in normal time; we run up to four to handle the awkward spring-forward case where the first correction overshoots the gap. Inside the gap the function picks the canonical real-instant that Intl.DateTimeFormat reports, and callers re-verify against their field set so cron schedules that fall in the gap fire at the next existing minute. Formatter construction is cached per IANA name to keep the hot path allocation-free.