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