SPB Git

Repository Public

zoned.tsx 1.5 KB 30 lines tsx

1/** Wall-clock → instant in an IANA zone, with Intl only (no Temporal in the browser). */2const fmts = new Map<string, Intl.DateTimeFormat>();3function fmt(tz: string) {4  let f = fmts.get(tz);5  if (!f) { f = new Intl.DateTimeFormat("en-GB", { timeZone: tz, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }); fmts.set(tz, f); }6  return f;7}8/** Offset (ms) of the zone at an instant: local wall time − UTC. */9export function tzOffsetMs(ms: number, tz: string): number {10  const p = fmt(tz).formatToParts(new Date(ms));11  const g = (t: string) => Number(p.find((x) => x.type === t)!.value);12  return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - Math.floor(ms / 1000) * 1000;13}14/**15 * Instant of `minutes` after local midnight of `ymd` in `tz`. A time skipped by a DST jump moves forward to the16 * first real instant after the gap; a repeated time resolves to its first occurrence (same as the server).17 */18export function zonedToMs(ymd: string, minutes: number, tz: string): number {19  const [y, m, d] = ymd.split("-").map(Number);20  const guess = Date.UTC(y, m - 1, d, 0, minutes);21  const off1 = tzOffsetMs(guess, tz);22  const t1 = guess - off1;23  const off2 = tzOffsetMs(t1, tz);24  if (off2 === off1) return t1;25  const t2 = guess - off2;26  if (tzOffsetMs(t2, tz) === off2) return t2;27  return Math.max(t1, t2);28}29export const zonedToIso = (ymd: string, minutes: number, tz: string) => new Date(zonedToMs(ymd, minutes, tz)).toISOString();30
Files main
Menu

Theme