@burglekitt/gmt
Version:
Temporal-based date and time utilities with timezone support and polyfill integration
80 lines (79 loc) • 3.67 kB
JavaScript
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 "";
}
}