UNPKG

@burglekitt/gmt

Version:

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

78 lines (77 loc) 5.49 kB
import { Temporal } from "@js-temporal/polyfill"; import type { CalendarSystem, Disambiguation, Offset } from "../types/index.js"; /** * Parse a bare ISO ZonedDateTime string or a GMT calendar-annotated ZonedDateTime string * (`"5784-06-15T14:30:00-05:00[u-ca=hebrew][America/New_York]"`, calendar-native fields, not * Temporal's own ISO-digit `[u-ca=...]` annotation convention) into a Temporal.ZonedDateTime. * Throws on invalid input — callers wrap this in try-catch per GMT's sentinel-return contract. * * The regex only proves shape; the real construction and validation happen in Temporal — * `parseCalendarDateValue` (`Temporal.PlainDate.from(fields, { overflow: "reject" })`) for the * calendar-native date half, and `Temporal.ZonedDateTime.from` for the recomposed ISO string * (which rejects unknown zones, stale offsets, and out-of-range times). See * `context/coding-standards.md`'s scoped manual-string-parsing exception, rule 2. * * --------------------------------------------------------------------------------------- * WHY THE DATE HALF IS FIELD-DECOMPOSED AND HANDED BACK TO TEMPORAL AS ISO DIGITS * --------------------------------------------------------------------------------------- * The `;era=` suffix can NEVER round-trip through Temporal, at any segment ordering, because it * is not valid RFC 9557 at all — `Temporal.PlainDate.from("0006-10-03[u-ca=japanese;era=reiwa]")` * throws `RangeError: invalid RFC 9557 string`, and so does every `Temporal.*.from` entry point * given the same suffix. Era-bearing calendars ("japanese", "ethiopic") therefore have to be * decomposed into `{ era, eraYear, month, day }` and constructed field-wise, which is exactly * what `parseCalendarDateValue` already does for the plain case — this function delegates the * whole date half to it rather than reimplementing era handling, the Ethiopic-family "ethioaa" * carrier routing, or the GMT-to-Temporal calendar-id mapping. * * The date half then travels back through Temporal as plain ISO digits * (`.withCalendar("iso8601").toString()`), so `Temporal.ZonedDateTime.from` resolves the zone, * offset and DST against a string it can actually parse. The calendar is re-attached to the * RESULT via `.withCalendar(...)`, which keeps downstream `.add`/`.until`/`.round` calendar-aware * (verified: `withTimeZone`/`round`/`with`/`startOfDay`/`toPlainDate` all preserve the tag). * * `isLeapSecond` runs before anything else, exactly as `isValidZonedDateTime` does: verified that * `Temporal.ZonedDateTime.from("2024-06-30T23:59:60+00:00[UTC]")` silently CLAMPS to `:59` rather * than throwing, so this guard is load-bearing rather than redundant. * * @param value bare ISO zoned datetime string, or GMT calendar-annotated zoned datetime string * @param options optional Temporal `disambiguation`/`offset`, passed through to * `Temporal.ZonedDateTime.from` so a caller's own DST/offset policy still applies * @returns Temporal.ZonedDateTime carrying the annotated calendar (or `iso8601` when bare) */ export declare function parseCalendarZonedValue(value: string, options?: { disambiguation?: Disambiguation; offset?: Offset; }): Temporal.ZonedDateTime; /** * Format a Temporal.ZonedDateTime as GMT's calendar-annotated (or bare ISO) zoned string in a * known target CalendarSystem — the zoned companion to `formatDateInCalendar`, and the ONLY * writer of the calendar-annotated zoned grammar anywhere in GMT. * * Never call `zdt.toString()` on a calendared value and never compose * `` `${zdt.toPlainDateTime()}[${zdt.timeZoneId}]` `` on one either: both emit Temporal's * `[timeZone][u-ca=...]` RFC 9557 ordering (verified: a Hebrew-calendared * `.toPlainDateTime().toString()` yields `"2024-03-25T14:30:00[u-ca=hebrew]"`), which is exactly * the ordering GMT rejects because reading it back misparses the digits as ISO. Route every * calendar-annotated zoned output through this function instead. * * Every field is RE-DERIVED from the actual `zdt` result and nothing but the IANA zone id is * copied (E7's D7-zoned, extending E5's D7): a single calendar-unit add can move the era AND the * UTC offset at once — verified, Japanese Heisei 31-04-05 in `Africa/Casablanca` `+1 month` lands * on Reiwa 1-05-05 inside a DST fold — so a tag or offset copied from the input would describe a * moment that no longer exists. * * Unlike `formatDateInCalendar`, this does NOT require `zdt` to already carry `calendar`'s own * Temporal calendar id — it re-calendars the value itself before reading the date fields. That is * deliberate: several `zoned/interval/*` functions synthesize boundary points via * `Temporal.Instant.prototype.toZonedDateTimeISO`, which always returns an `iso8601`-calendared * value. Re-calendaring here means a synthesized boundary can never silently emit a bare ISO * string into an otherwise calendar-tagged result set — one class of bug removed by construction * rather than by remembering to re-attach the tag at every synthesis site. The instant, wall time * and zone are untouched; only which calendar the date fields resolve through changes. * * @param zdt Temporal.ZonedDateTime to format * @param calendar the CalendarSystem to express `zdt`'s date fields in * @returns GMT's calendar-annotated (or bare ISO, for "gregorian") zoned datetime string */ export declare function formatZonedInCalendar(zdt: Temporal.ZonedDateTime, calendar: CalendarSystem): string;