UNPKG

@burglekitt/gmt

Version:

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

80 lines (79 loc) 3.67 kB
import { Temporal } from "@js-temporal/polyfill"; import { normalizeDateTime, resolveRelativeRounding } from "../../internal/index.js"; import { isValidUtc } from "../../utc/validate/index.js"; import { isValidZonedDateTime } from "../validate/index.js"; const AUTO_UNITS = [ { unit: "second", maxSeconds: 60 }, { unit: "minute", maxSeconds: 3_600 }, { unit: "hour", maxSeconds: 86_400 }, { unit: "day", maxSeconds: Infinity }, ]; /** * Format the relative time between a zoned date-time and a reference instant. * * - Auto-picks the display unit (second through year) based on the distance, unless * `largestUnit` forces one. * - `roundingMethod` controls how the distance rounds to the display unit. * * @param value ZonedDateTime ISO string to format * @param locale optional: BCP 47 locale tag * @param options optional: { style, numeric, largestUnit, roundingMethod, reference } * @returns the formatted relative-time string, or "" on invalid input * * @example formatRelativeZoned("2026-03-08T01:00:00-05:00[America/New_York]", "en-US") // "tomorrow" * @example formatRelativeZoned(value, "en-US", { roundingMethod: "floor" }) // rounds toward the earlier boundary * @example formatRelativeZoned("not-a-date") // "" */ export function formatRelativeZoned(value, locale, options = {}) { if (!isValidZonedDateTime(value)) return ""; // String reference must be a valid ZonedDateTime or UTC ISO string. if (typeof options.reference === "string" && !isValidZonedDateTime(options.reference) && !isValidUtc(options.reference)) return ""; if (typeof options.reference === "number" && !Number.isFinite(options.reference)) return ""; try { const valueZDT = Temporal.ZonedDateTime.from(value); const valueInstant = valueZDT.toInstant(); let refZDT; if (options.reference == null) { // "now" in value's own zone — keeps the calendar context consistent. refZDT = Temporal.Now.zonedDateTimeISO(valueZDT.timeZoneId); } else if (typeof options.reference === "string") { // UTC string → place into value's zone for a consistent calendar anchor. // ZonedDateTime string → keep its own zone; Temporal handles cross-zone diffs. // isValidUtc covers both `Z` and `z` suffixes (the regex accepts [Zz]). refZDT = isValidUtc(options.reference) ? Temporal.Instant.from(options.reference).toZonedDateTimeISO(valueZDT.timeZoneId) : Temporal.ZonedDateTime.from(options.reference); } else { // Numeric epoch (ms) → place into value's zone. refZDT = Temporal.Instant.fromEpochMilliseconds(options.reference).toZonedDateTimeISO(valueZDT.timeZoneId); } const diff = valueInstant.since(refZDT.toInstant()); const absSeconds = Math.abs(diff.total("second")); const unit = options.largestUnit ?? AUTO_UNITS.find((t) => absSeconds < t.maxSeconds)?.unit ?? "day"; let amount; try { amount = resolveRelativeRounding(diff.total(unit), options.roundingMethod); } catch { // month/year are calendrical and need a relativeTo anchor amount = resolveRelativeRounding(diff.total({ unit, relativeTo: refZDT }), options.roundingMethod); } return normalizeDateTime(new Intl.RelativeTimeFormat(locale, { numeric: options.numeric ?? "auto", style: options.style ?? "long", }).format(amount, unit)); } catch { return ""; } }