const BRUSSELS_TIME_ZONE = "Europe/Brussels"; const partsFormatter = new Intl.DateTimeFormat("en-CA", { timeZone: BRUSSELS_TIME_ZONE, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23", }); function partsAt(value: Date): Record { return Object.fromEntries( partsFormatter.formatToParts(value) .filter((part) => part.type !== "literal") .map((part) => [part.type, part.value]), ); } export function toBrusselsDateTimeLocal(value: Date): string { const parts = partsAt(value); return `${toBrusselsDate(value)}T${parts.hour}:${parts.minute}`; } /** Render an instant as the Fleet Ops Europe/Brussels calendar date (YYYY-MM-DD). */ export function toBrusselsDate(value: Date): string { const parts = partsAt(value); return `${parts.year}-${parts.month}-${parts.day}`; } export function brusselsDateTimeFromNow(hours: number): string { const value = new Date(Date.now() + hours * 60 * 60 * 1000); value.setUTCMinutes(0, 0, 0); return toBrusselsDateTimeLocal(value); } /** Convert a Fleet Ops Europe/Brussels wall-clock value to an unambiguous UTC instant. */ export function brusselsLocalToIso(value: string): string { const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(value); if (!match) throw new Error("Invalid Brussels date-time"); const [, year, month, day, hour, minute] = match; const wallClockUtc = Date.UTC(+year, +month - 1, +day, +hour, +minute); let candidate = wallClockUtc; for (let iteration = 0; iteration < 3; iteration += 1) { const parts = partsAt(new Date(candidate)); const represented = Date.UTC(+parts.year, +parts.month - 1, +parts.day, +parts.hour, +parts.minute); candidate += wallClockUtc - represented; } const result = new Date(candidate); // Date.UTC normalises impossible calendar values and Brussels' spring transition // contains a wall-clock hour that does not exist. Never silently shift either one. if (toBrusselsDateTimeLocal(result) !== value) { throw new Error("Invalid or non-existent Brussels date-time"); } return result.toISOString(); } export function tryBrusselsLocalToIso(value: string): string | null { try { return brusselsLocalToIso(value); } catch { return null; } } /** Start of the given Brussels calendar day (YYYY-MM-DD) as a UTC ISO instant. */ export function brusselsDayStartIso(day: string): string { return brusselsLocalToIso(`${day}T00:00`); } /** Start of the day *after* the given Brussels calendar day, i.e. an exclusive upper bound. */ export function brusselsNextDayStartIso(day: string): string { const [year, month, date] = day.split("-").map(Number); const next = new Date(Date.UTC(year, month - 1, date + 1)); return brusselsLocalToIso(`${next.toISOString().slice(0, 10)}T00:00`); }