SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
3.6 KB · 114 lines typescript
Raw Blame History
1// Time-zone utilities without external dependencies (Intl only).23const dtfCache = new Map<string, Intl.DateTimeFormat>();45function dtf(timeZone: string): Intl.DateTimeFormat {6  let f = dtfCache.get(timeZone);7  if (!f) {8    f = new Intl.DateTimeFormat("en-US", {9      timeZone,10      hourCycle: "h23",11      year: "numeric",12      month: "2-digit",13      day: "2-digit",14      hour: "2-digit",15      minute: "2-digit",16      second: "2-digit",17      weekday: "short",18    });19    dtfCache.set(timeZone, f);20  }21  return f;22}2324export interface ZonedParts {25  year: number;26  month: number; // 1-1227  day: number;28  hour: number;29  minute: number;30  second: number;31  weekday: number; // 0 = Sunday32  date: string; // YYYY-MM-DD33  time: string; // HH:MM34}3536const WEEKDAYS: Record<string, number> = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };3738export function zonedParts(ms: number, timeZone: string): ZonedParts {39  const parts = dtf(timeZone).formatToParts(new Date(ms));40  const get = (t: string) => parts.find((p) => p.type === t)?.value ?? "";41  const year = Number(get("year"));42  const month = Number(get("month"));43  const day = Number(get("day"));44  const hour = Number(get("hour")) % 24;45  const minute = Number(get("minute"));46  const second = Number(get("second"));47  const pad = (n: number) => String(n).padStart(2, "0");48  return {49    year,50    month,51    day,52    hour,53    minute,54    second,55    weekday: WEEKDAYS[get("weekday")] ?? 0,56    date: `${year}-${pad(month)}-${pad(day)}`,57    time: `${pad(hour)}:${pad(minute)}`,58  };59}6061/** Offset (ms) of `timeZone` relative to UTC at instant `ms`. */62export function tzOffsetMs(ms: number, timeZone: string): number {63  const p = zonedParts(ms, timeZone);64  const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);65  return asUtc - Math.floor(ms / 1000) * 1000;66}6768/**69 * Interpret a wall-clock time in `timeZone` as a UTC instant.70 * Accepts "YYYY-MM-DD", "YYYY-MM-DDTHH:MM[:SS[.sss]]" or "YYYY-MM-DD HH:MM[:SS]".71 */72export function zonedTimeToUtc(local: string, timeZone: string): number | null {73  const m = local74    .trim()75    .match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?)?$/);76  if (!m) return null;77  const [_, y, mo, d, h = "0", mi = "0", s = "0", frac = "0"] = m;78  const guess = Date.UTC(+y!, +mo! - 1, +d!, +h, +mi, +s, +frac.padEnd(3, "0"));79  // Two-pass correction handles DST edges well enough for market data.80  const off1 = tzOffsetMs(guess, timeZone);81  const utc1 = guess - off1;82  const off2 = tzOffsetMs(utc1, timeZone);83  return guess - off2;84}8586/** Local wall-clock "HH:MM" of `timeZone` → ms since local midnight. */87export function hmToMs(hm: string): number {88  const [h, m] = hm.split(":").map(Number);89  return ((h ?? 0) * 60 + (m ?? 0)) * 60_000;90}9192/** Parse a variety of timestamp encodings; returns ms epoch or null. */93export function parseTimestamp(input: unknown): number | null {94  if (input == null) return null;95  if (typeof input === "number") {96    if (!Number.isFinite(input)) return null;97    if (input > 1e17) return Math.floor(input / 1e6); // ns98    if (input > 1e14) return Math.floor(input / 1e3); // µs99    if (input > 1e11) return Math.floor(input); // ms100    return Math.floor(input * 1000); // s101  }102  if (typeof input === "string") {103    const t = input.trim();104    if (/^\d+(\.\d+)?$/.test(t)) return parseTimestamp(Number(t));105    const ms = Date.parse(t);106    return Number.isFinite(ms) ? ms : null;107  }108  return null;109}110111export function floorTo(ms: number, bucketMs: number): number {112  return Math.floor(ms / bucketMs) * bucketMs;113}114