UNPKG

@burglekitt/gmt

Version:

Temporal-based date and time utilities with timezone support and polyfill integration

108 lines (107 loc) 3.97 kB
import { Temporal } from "@js-temporal/polyfill"; import { joinDateTimeConnector, normalizeDateTime } from "../../internal/index.js"; import { normalizeTimeZone } from "../../internal/normalizeTimeZone.js"; import { isValidUtc } from "../../utc/validate/index.js"; const ABS_DAY_THRESHOLD = 6; function toInstant(raw, epochUnit) { let n; if (typeof raw === "number") { n = raw; } else if (typeof raw === "string") { const trimmed = raw.trim(); // Mirror formatUnix.parseEpochMs: only accept integer-looking strings, // so "" / "not-a-date" / "12.5" don't silently coerce to 0/12. if (!/^-?\d+$/.test(trimmed)) return null; n = Number(trimmed); } else { return null; } if (!Number.isFinite(n)) return null; try { const ms = epochUnit === "seconds" ? n * 1000 : n; return Temporal.Instant.fromEpochMilliseconds(ms); } catch { return null; } } /** * Format a unix epoch value as a relative day label plus time-of-day, e.g. * "Tomorrow at 2:30 PM" — the unix counterpart of `formatCalendar`. See * that function's JSDoc for the day-label/threshold/connector design; this * variant compares calendar days and renders the clock time in `timeZone` * (default `"UTC"`). * * @param value unix epoch (string or number, per `epochUnit`) to format * @param locale optional: BCP 47 locale tag * @param options optional: { epochUnit, reference, timeZone, timeStyle } * @returns the formatted calendar string, or "" on invalid input * * @example formatCalendarUnix(1710685845000, "en-US", { epochUnit: "milliseconds", timeZone: "America/New_York" }) // day label + time relative to "now", or the absolute fallback beyond the ±6-day threshold * @example formatCalendarUnix(value, "en-US", { reference: 1710685000000 }) // e.g. "tomorrow at 2:30 PM" * @example formatCalendarUnix("not-a-number") // "" */ export function formatCalendarUnix(value, locale, options = {}) { const epochUnit = options.epochUnit ?? "milliseconds"; const target = toInstant(value, epochUnit); if (target === null) return ""; let reference; if (options.reference === undefined) { try { reference = Temporal.Now.instant(); } catch { return ""; } } else if (typeof options.reference === "string") { const numericRef = toInstant(options.reference, epochUnit); if (numericRef !== null) { reference = numericRef; } else if (isValidUtc(options.reference)) { try { reference = Temporal.Instant.from(options.reference); } catch { return ""; } } else { return ""; } } else { const ref = toInstant(options.reference, epochUnit); if (ref === null) return ""; reference = ref; } try { const timeZone = normalizeTimeZone(options.timeZone); const timeStyle = options.timeStyle ?? "short"; const targetDate = target.toZonedDateTimeISO(timeZone).toPlainDate(); const referenceDate = reference.toZonedDateTimeISO(timeZone).toPlainDate(); const diffDays = targetDate.since(referenceDate).days; const epochMilliseconds = Number(target.epochMilliseconds); if (Math.abs(diffDays) > ABS_DAY_THRESHOLD) { return normalizeDateTime(new Intl.DateTimeFormat(locale, { dateStyle: "long", timeStyle, timeZone, }).format(epochMilliseconds)); } const dayLabel = new Intl.RelativeTimeFormat(locale, { numeric: "auto", }).format(diffDays, "day"); return normalizeDateTime(joinDateTimeConnector(epochMilliseconds, timeZone, locale, dayLabel, timeStyle)); } catch { return ""; } }