@burglekitt/gmt
Version:
Temporal-based date and time utilities with timezone support and polyfill integration
119 lines (118 loc) • 4.33 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import { normalizeDateTime } from "../../internal/normalizeDateTime.js";
import { normalizeTimeZone } from "../../internal/normalizeTimeZone.js";
import { resolveRelativeRounding } from "../../internal/resolveRelativeRounding.js";
import { isValidUtc } from "../../utc/validate/index.js";
const AUTO_UNITS = [
{ unit: "second", maxSeconds: 60 },
{ unit: "minute", maxSeconds: 3_600 },
{ unit: "hour", maxSeconds: 86_400 },
{ unit: "day", maxSeconds: Infinity },
];
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 the relative time between a unix epoch value 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 unix epoch (string or number, per `epochUnit`) to format
* @param locale optional: BCP 47 locale tag
* @param options optional: { style, numeric, largestUnit, roundingMethod, epochUnit, reference, timeZone }
* @returns the formatted relative-time string, or "" on invalid input
*
* @example formatRelativeUnix(1710685845000, "en-US", { epochUnit: "milliseconds" }) // "3 years ago"
* @example formatRelativeUnix(value, "en-US", { roundingMethod: "floor" }) // rounds toward the earlier boundary
* @example formatRelativeUnix("not-a-number") // ""
*/
export function formatRelativeUnix(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") {
// String references can be a numeric unix epoch ("1709164800000") OR a
// UTC ISO string ("2024-02-29T00:00:00Z"). Try the numeric path first to
// match formatUnix's symmetry, then fall back to UTC.
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 diff = target.since(reference);
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.
// Defer timezone normalization until we know we need it.
const tz = normalizeTimeZone(options.timeZone);
amount = resolveRelativeRounding(diff.total({ unit, relativeTo: reference.toZonedDateTimeISO(tz) }), options.roundingMethod);
}
return normalizeDateTime(new Intl.RelativeTimeFormat(locale, {
numeric: options.numeric ?? "auto",
style: options.style ?? "long",
}).format(amount, unit));
}
catch {
return "";
}
}