import { InvalidNominalLocalError } from "./errors.js"; import type { NominalLocal, WallClock } from "./timezone.js"; /** Format a wall clock as a nominal local date-time string, `YYYY-MM-DDTHH:mm`. */ export function formatNominal(wall: WallClock): NominalLocal { const y = String(wall.year).padStart(4, "0"); const mo = String(wall.month).padStart(2, "0"); const d = String(wall.day).padStart(2, "0"); const h = String(wall.hour).padStart(2, "0"); const mi = String(wall.minute).padStart(2, "0"); return `${y}-${mo}-${d}T${h}:${mi}`; } const NOMINAL_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/; /** * Parse a nominal local date-time string back into a wall clock. Rejects * malformed strings and impossible calendar values with * `InvalidNominalLocalError`. Note the string may denote a wall time that does * not exist in a zone (gap) or exists twice (fold); existence is a * materialization concern, not a parse concern. */ export function parseNominal(nominal: NominalLocal): WallClock { const match = NOMINAL_RE.exec(nominal); if (!match) { throw new InvalidNominalLocalError(`malformed nominal local date-time: "${nominal}"`); } const wall: WallClock = { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]), hour: Number(match[4]), minute: Number(match[5]), second: match[6] === undefined ? 0 : Number(match[6]), }; if (wall.month < 1 || wall.month > 12) { throw new InvalidNominalLocalError(`month out of range in "${nominal}"`); } if (wall.day < 1 || wall.day > 31) { throw new InvalidNominalLocalError(`day out of range in "${nominal}"`); } if (wall.hour > 23 || wall.minute > 59 || wall.second > 59) { throw new InvalidNominalLocalError(`time of day out of range in "${nominal}"`); } return wall; } /** Total ordering over wall clocks; safe because fields are validated. */ export function compareWall(a: WallClock, b: WallClock): number { if (a.year !== b.year) return a.year < b.year ? -1 : 1; if (a.month !== b.month) return a.month < b.month ? -1 : 1; if (a.day !== b.day) return a.day < b.day ? -1 : 1; if (a.hour !== b.hour) return a.hour < b.hour ? -1 : 1; if (a.minute !== b.minute) return a.minute < b.minute ? -1 : 1; return a.second < b.second ? -1 : a.second > b.second ? 1 : 0; } export function wallEquals(a: WallClock, b: WallClock): boolean { return compareWall(a, b) === 0; } /** Milliseconds between two wall readings when interpreted as UTC. */ export function wallAsEpochMs(wall: WallClock): number { return Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second); }