import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; import utc from "dayjs/plugin/utc"; dayjs.extend(utc); dayjs.extend(customParseFormat); const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/; const NUMERIC_RE = /^-?\d+$/; export const nowMs = (): number => dayjs().valueOf(); export const parseToMs = (input: string | number): number => { if (typeof input === "number") { if (!Number.isFinite(input)) throw new Error(`Invalid numeric timestamp: ${input}`); return input; } const trimmed = input.trim(); if (trimmed === "") throw new Error("Empty date input"); if (NUMERIC_RE.test(trimmed)) { const n = Number(trimmed); if (!Number.isFinite(n)) throw new Error(`Invalid numeric timestamp: ${input}`); return n; } const d = dayjs.utc(trimmed); if (!d.isValid()) throw new Error(`Cannot parse date: ${input}`); return d.valueOf(); }; export interface RangeBounds { sinceMs?: number; untilMs?: number; } export const parseRangeBounds = (since?: string, until?: string): RangeBounds => { const out: RangeBounds = {}; if (since !== undefined && since !== "") { out.sinceMs = parseToMs(since); } if (until !== undefined && until !== "") { const trimmed = until.trim(); out.untilMs = DATE_ONLY_RE.test(trimmed) ? dayjs.utc(trimmed).endOf("day").valueOf() : parseToMs(trimmed); } if (out.sinceMs !== undefined && out.untilMs !== undefined && out.sinceMs > out.untilMs) { throw new Error(`Invalid range: since (${since ?? ""}) is after until (${until ?? ""})`); } return out; }; export const formatDate = (ms: number): string => dayjs(ms).format("YYYY-MM-DD"); export const formatIso = (ms: number): string => dayjs(ms).format(); export const formatDateTime = (ms: number): string => dayjs(ms).format("YYYY-MM-DD HH:mm:ss"); /** Parse Y/M/D (single or zero-padded) in UTC. Returns null on failure. */ export const parseYmdUtc = (year: string, month: string, day: string): number | null => { const d = dayjs.utc(`${year}-${month}-${day}`, "YYYY-M-D", true); return d.isValid() ? d.valueOf() : null; };