UNPKG

@burglekitt/gmt

Version:

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

61 lines (60 loc) 3.62 kB
import { Temporal } from "@js-temporal/polyfill"; import { resolveOverflow } from "../../internal/index.js"; import { getSystemTimeZone } from "../../zoned/get/index.js"; import { isValidTimeZone } from "../../zoned/validate/index.js"; /** * Return a Unix epoch value with the given `fields` set on `value`, interpreted in `timeZone`. * * - Converts to ZonedDateTime, wraps `Temporal.ZonedDateTime.prototype.with()` (resolving every * supplied field in a single atomic overflow pass), then converts back to epoch. This is the * safe alternative to composing `addUnix()` calls field-by-field — see `setZoned`'s doc for * why order-independent field resolution matters, and why `disambiguation`/`offset` require * `.with()` rather than arithmetic. * - `fields` may set any of `year`, `month`, `monthCode`, `day`, `hour`, `minute`, `second`, * `millisecond`, `microsecond`, `nanosecond`, `era`, and/or `eraYear`; omitted fields keep * their current value. An empty object is a no-op. * - `overflow` ("constrain" (default) | "reject") controls out-of-range results, e.g. setting * `month: 2` on a value whose `day` is 31: "constrain" clamps to Feb 29/28, "reject" throws * (resulting in null). * - Returns null for invalid input. * * @param value Unix timestamp (number) * @param fields Partial<Temporal.ZonedDateTimeLike> object (excluding calendar/timeZone/offset) specifying fields to set * @param options optional: epochUnit ("seconds" | "milliseconds"), timeZone (IANA), overflow ("constrain" | "reject"), disambiguation ("compatible" | "earlier" | "later" | "reject"), offset ("prefer" | "use" | "ignore" | "reject", default "ignore") * @returns Unix epoch number with fields set, or null on invalid input * * @example setUnix(1710072000000, { hour: 9 }, { timeZone: "UTC" }) // 1710061200000 (2024-03-10T09:00:00Z) * @example setUnix(1706659200000, { year: 2025 }, { timeZone: "UTC" }) // 1738281600000 (2025-01-31T00:00:00Z) * @example setUnix(1706659200000, {}, { timeZone: "UTC" }) // 1706659200000 (empty fields object is a no-op) * @example setUnix(NaN, { hour: 9 }) // null */ export function setUnix(value, fields, options) { const epochUnit = options?.epochUnit ?? "milliseconds"; const timeZone = options?.timeZone ?? getSystemTimeZone(); if (!timeZone || !isValidTimeZone(timeZone)) return null; if (!Number.isFinite(value) || !Number.isInteger(value)) return null; const overflow = resolveOverflow(options?.overflow); const disambiguation = options?.disambiguation ?? "compatible"; // "ignore" (not Temporal's own "prefer" default) so `disambiguation` actually takes effect — // see C3 / docs/dst-disambiguation.md: with "prefer", the source's still-valid offset is kept // and disambiguation is silently never consulted. const offset = options?.offset ?? "ignore"; try { const epochMs = epochUnit === "seconds" ? value * 1000 : value; const instant = Temporal.Instant.fromEpochMilliseconds(epochMs); const zoned = instant.toZonedDateTimeISO(timeZone); // Temporal.ZonedDateTime.prototype.with() throws on an empty fields object ("no supported // properties found") rather than treating it as a no-op, so short-circuit here. const result = Object.keys(fields).length === 0 ? zoned : zoned.with(fields, { overflow, disambiguation, offset }); return epochUnit === "seconds" ? Math.floor(result.epochMilliseconds / 1000) : result.epochMilliseconds; } catch { return null; } }