UNPKG

@burglekitt/gmt

Version:

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

305 lines (210 loc) 15.1 kB
--- name: convert-types description: > Convert between temporal types, unix time, and UTC representations. Use convertPlainDateTimeToZoned (with optional disambiguation for DST gaps/ overlaps), convertZonedToPlainDateTime, convertUtcToUnix, and convertDateToCalendar to express a date in a non-Gregorian CalendarSystem (gregorian, hebrew, islamic-civil, islamic-tabular, islamic-umalqura, japanese, buddhist, taiwan, persian, indian, ethiopic, ethiopic-amete-alem, coptic). sources: - 'burglekitt/gmt:packages/gmt/src/plain/convert/index.ts' - 'burglekitt/gmt:packages/gmt/src/zoned/convert/index.ts' - 'burglekitt/gmt:packages/gmt/src/unix/convert/index.ts' - 'burglekitt/gmt:packages/gmt/src/utc/convert/index.ts' - 'burglekitt/gmt:packages/gmt/src/types/calendar-system.ts' metadata: type: core library: '@burglekitt/gmt' library_version: '1.14.1' --- # Convert Temporal Types Use this skill when you need to convert between different temporal representations. ## Setup ```ts import { convertPlainDateTimeToZoned, convertZonedToPlainDateTime } from "@burglekitt/gmt/zoned"; import { convertUtcToUnix, convertUnixToUtc } from "@burglekitt/gmt/unix"; import { convertUtcToZoned, convertZonedToUtc } from "@burglekitt/gmt/utc"; ``` ## Core Patterns ### Convert plain to zoned datetime ```ts import { convertPlainDateTimeToZoned } from "@burglekitt/gmt/zoned"; const zoned = convertPlainDateTimeToZoned("2024-03-15T14:30:45", "America/New_York"); // "2024-03-15T14:30:45[America/New_York]" ``` Twice a year, DST creates local times that don't exist (spring-forward gap) or happen twice (fall-back overlap). Pass `disambiguation` (`"compatible"` (default) | `"earlier"` | `"later"` | `"reject"`) to control resolution instead of silently guessing — see the [Zoned Date Operations skill](../zoned-date-ops/SKILL.md) for the full gap/overlap walkthrough: ```ts convertPlainDateTimeToZoned("2024-03-10T02:30:00", "America/New_York", { disambiguation: "reject", }); // "" — 2024-03-10T02:30:00 doesn't exist in America/New_York (spring-forward gap) ``` ### Convert zoned to plain datetime ```ts import { convertZonedToPlainDateTime } from "@burglekitt/gmt/zoned"; const plain = convertZonedToPlainDateTime("2024-03-15T14:30:45[America/New_York]"); // "2024-03-15T14:30:45" ``` ### Convert UTC to Unix epoch (seconds) ```ts import { convertUtcToUnix } from "@burglekitt/gmt/unix"; const unix = convertUtcToUnix("2024-03-15T14:30:45"); // 1710504645 ``` ### Convert Unix epoch to UTC (seconds) ```ts import { convertUnixToUtc } from "@burglekitt/gmt/unix"; const utc = convertUnixToUtc(1710504645); // "2024-03-15T14:30:45" ``` ### Convert UTC datetime to Unix milliseconds ```ts import { convertUtcToUnixMs } from "@burglekitt/gmt/unix"; const unixMs = convertUtcToUnixMs("2024-03-15T14:30:45"); // 1710504645000 ``` ### Convert Unix milliseconds to UTC datetime ```ts import { convertUnixMsToUtc } from "@burglekitt/gmt/unix"; const utc = convertUnixMsToUtc(1710504645000); // "2024-03-15T14:30:45" ``` ### Convert zoned to UTC ```ts import { convertZonedToUtc } from "@burglekitt/gmt/zoned"; const utc = convertZonedToUtc("2024-03-15T14:30:45[America/New_York]"); // "2024-03-15T19:30:45" ``` ### Convert UTC to zoned ```ts import { convertUtcToZoned } from "@burglekitt/gmt/utc"; const zoned = convertUtcToZoned("2024-03-15T14:30:45", "America/New_York"); // "2024-03-15T10:30:45[America/New_York]" ``` ### Convert between timezones ```ts import { convertZonedToZoned } from "@burglekitt/gmt/zoned"; const converted = convertZonedToZoned( "2024-03-15T14:30:45[America/New_York]", "Europe/London" ); // "2024-03-15T18:30:45[Europe/London]" ``` ### Convert a date to a non-Gregorian calendar system ```ts import { convertDateToCalendar } from "@burglekitt/gmt/plain"; const hebrew = convertDateToCalendar("2024-10-03", "hebrew"); // "5785-01-01[u-ca=hebrew]" — Rosh Hashanah 5785, calendar-native year/month/day const back = convertDateToCalendar(hebrew, "gregorian"); // "2024-10-03" — round-trips back to a bare ISO string const umalqura = convertDateToCalendar("2024-10-03", "islamic-umalqura"); // "1446-03-30[u-ca=islamic-umalqura]" — Saudi civil calendar const buddhist = convertDateToCalendar("2024-10-03", "buddhist"); // "2567-10-03[u-ca=buddhist]" — fixed +543 year offset const japanese = convertDateToCalendar("2024-10-03", "japanese"); // "0006-10-03[u-ca=japanese;era=reiwa]" — era-relative year, not proleptic 2024 const ethiopic = convertDateToCalendar("2024-10-03", "ethiopic"); // "2017-01-23[u-ca=ethiopic;era=ethiopic]" — era-relative year, not proleptic 7517 const coptic = convertDateToCalendar("2024-10-03", "coptic"); // "1741-01-23[u-ca=coptic]"13-month structure, own epoch (AD 284) ``` `CalendarSystem` is `"gregorian" | "hebrew" | "islamic-civil" | "islamic-tabular" | "islamic-umalqura" | "japanese" | "buddhist" | "taiwan" | "persian" | "indian" | "ethiopic" | "ethiopic-amete-alem" | "coptic"` today (extended by later Story Group E stories). The annotated string carries the target calendar's own year/month/day — not the ISO/Gregorian digits Temporal's own `[u-ca=...]` string convention keeps — so a Hebrew year like 5785 is visible directly in the string. A plain, unannotated string is always the `"gregorian"` calendar and works with every other GMT function unchanged. Every calendar except the Ethiopic family (below) is resolved through Temporal's own built-in calendar support (`PlainDate.prototype.withCalendar`) — GMT ports no leap-year tables or arithmetic of its own, for Islamic or era-based solar calendars any more than for Hebrew. The three Islamic variants are **not interchangeable**: `"islamic-civil"` and `"islamic-tabular"` use different fixed leap-year cycles one day apart in epoch, and `"islamic-umalqura"` is a tabulated calendar (the Saudi civil calendar) that can diverge from both by more than a fixed offset on a given date — `convertDateToCalendar` never approximates one variant with another's math. Note that GMT's own id for the tabular variant, `"islamic-tabular"`, differs from Temporal's internal id for the same calendar (`"islamic-tbla"`); the annotated string always reads `[u-ca=islamic-tabular]` regardless. `"buddhist"` and `"taiwan"` are fixed year-offset calendars over the same Gregorian day/month structure (`+543`, and reset-at-1912 respectively); `"persian"` and `"indian"` are distinct solar calendars with their own leap-year rules (Persian: a 33-year cycle; Indian: aligned to the Gregorian leap-year rule, not an independent one). `"japanese"` is the odd one out: Temporal's `.year` for it stays **proleptic** across imperial era changes rather than resetting the way the calendar's own numbering does, so `convertDateToCalendar` tags it with `.eraYear` and an era name instead of a plain year — `"0006-10-03[u-ca=japanese;era=reiwa]"`, not `"2024-10-03[u-ca=japanese]"`. GMT also does not reject pre-Meiji (before 1868-10-23) dates the way `@internationalized/date` does — Temporal resolves them under a synthetic `"japanese"` era and GMT passes that through rather than adding validation just to reproduce another library's gap. `"ethiopic"`, `"ethiopic-amete-alem"`, and `"coptic"` share one 13-month structure (12 x 30-day months + a 5/6-day Pagume/Nasie 13th month) but differ in epoch, and are the **one exception** to "resolved through Temporal's native calendar support": `@js-temporal/polyfill` resolves these two calendars' year/era via `Intl.DateTimeFormat`-derived era-name matching, and that matching breaks under newer ICU versions (confirmed: every read/write of Temporal's `"ethiopic"`/`"coptic"` calendar ids throws under ICU >= 78 — the ICU version both Node 22 and Node 24 bundle; this is an ICU-version boundary, not a Node-major one). GMT routes around this by reading/writing them through Temporal's `"ethioaa"` id instead (pure arithmetic, no era, unaffected by the bug — month/day are identical across the whole family) and computing each calendar's own year/era with GMT-owned arithmetic. `"ethiopic"` is era-based like `"japanese"` (resets at its own epoch, ~AD 8): `"2017-01-23[u-ca=ethiopic;era=ethiopic]"`, not a 5-digit proleptic year. `"ethiopic-amete-alem"` is the same calendar counted continuously from a much older epoch (~5493 BCE) and never resets: `"7517-01-23[u-ca=ethiopic-amete-alem]"` for the same date. `"coptic"` has its own epoch (AD 284, the Diocletian/Martyrs era) and a plain native year. See `internal/ethiopicFamilyCalendar.ts` for the implementation and full rationale. **The annotated string this function produces feeds directly into calendar-aware arithmetic (E5, issue #78):** `addDate`/`subtractDate`/`diffDate`/`diffDateAsDuration`, every `Date`-suffixed `plain/interval/*` function, and `duration/`'s `relativeTo` option all accept it — see the `calculate-dates`, `interval-ops`, and `durations` skills for how each resolves calendar-unit arithmetic in the value's own calendar. `zoned/`, `utc/`, and `unix/` reject it outright; there is no calendar-annotated `PlainDateTime`/`ZonedDateTime` grammar. ## Calendar Conversion - `convertDateToCalendar(value, calendar)` — PlainDate in any of the 13 calendar systems, e.g. `"5785-01-01[u-ca=hebrew]"`. - `convertZonedToCalendar(value, calendar)` — the zoned equivalent, keeping the instant, offset and IANA zone unchanged: `"5785-01-01T14:30:45-04:00[u-ca=hebrew][America/New_York]"`. Both chain: pass either function's output back in with a different target calendar. Converting to `"gregorian"` returns a bare, unannotated ISO string. Do **not** try to bridge between them by string surgery — a calendar-annotated `PlainDate` is not a valid input to any zoned conversion, and a calendar-annotated zoned string is not a valid `PlainDate`. Convert the value in its own namespace. ## Common Mistakes ### Building a calendar-annotated zoned string by hand GMT's zoned calendar grammar puts `[u-ca=...]` **before** `[timeZone]` — the reverse of RFC 9557 and of Temporal's own `toString()`: ``` 5784-06-15T14:30:00-05:00[u-ca=hebrew][America/New_York] // correct 5784-06-15T14:30:00-05:00[America/New_York][u-ca=hebrew] // WRONG — silently misparses in Temporal ``` Always produce these with `convertZonedToCalendar`, never by concatenation. See the `zoned-date-ops` skill's Common Mistakes for the full trap. ### HIGH Using Date.getTime() for conversion Wrong: ```ts const unix = new Date("2024-03-15T14:30:45").getTime() / 1000; // manual conversion ``` Correct: ```ts import { convertUtcToUnix } from "@burglekitt/gmt/unix"; const unix = convertUtcToUnix("2024-03-15T14:30:45"); // proper conversion ``` Source: AGENTS.md — Never use Date APIs ### HIGH Mixing epoch seconds and milliseconds Wrong: ```ts const timestamp = 1710504645; // seconds new Date(timestamp); // treats as milliseconds, wrong date ``` Correct: ```ts import { convertUnixToUtc } from "@burglekitt/gmt/unix"; const timestamp = 1710504645; // seconds const utc = convertUnixToUtc(timestamp); ``` Source: packages/gmt/src/unix/convert/convertUnixToUtc.ts — expects seconds ### MEDIUM Not handling conversion errors Wrong: ```ts const zoned = convertPlainDateTimeToZoned("invalid", "America/New_York"); // Assume zoned is always valid process(zoned); ``` Correct: ```ts import { convertPlainDateTimeToZoned, isValidTimeZone } from "@burglekitt/gmt/zoned"; if (!isValidTimeZone("America/New_York")) { throw new Error("Invalid timezone"); } const zoned = convertPlainDateTimeToZoned("2024-03-15T14:30:45", "America/New_York"); if (!zoned) { throw new Error("Conversion failed"); } ``` Source: packages/gmt/src/zoned/convert/convertPlainDateTimeToZoned.ts — Returns "" on error ### MEDIUM Assuming a `[u-ca=...]` string means Temporal's own convention Wrong: ```ts // Assuming the digits are still the ISO/Gregorian year, like Temporal.PlainDate's own // toString() would produce const hebrew = convertDateToCalendar("2024-10-03", "hebrew"); // "5785-01-01[u-ca=hebrew]" const year = hebrew.slice(0, 4); // "5785" is the Hebrew year, NOT 2024 — don't assume ISO digits ``` Correct: treat the annotated string as GMT's own format (calendar-native fields, produced/consumed only by `convertDateToCalendar`) — never hand-parse it or assume it matches `Temporal.PlainDate.prototype.toString()`'s own `[u-ca=...]` output, which keeps the ISO digits and only tags the calendar. Round-trip it through `convertDateToCalendar` itself instead of extracting fields manually. Source: packages/gmt/src/plain/convert/convertDateToCalendar.ts — see JSDoc for the full rationale ### MEDIUM Assuming `"japanese"`'s annotated year is proleptic like every other calendar Wrong: ```ts // Assuming the year digits are the ISO/Gregorian year, the way "buddhist"/"taiwan"'s // digits are always a fixed transform of it const japanese = convertDateToCalendar("2024-10-03", "japanese"); // "0006-10-03[u-ca=japanese;era=reiwa]" — "0006" is year 6 of the Reiwa era, NOT 2024 const gregorianGuess = 2024 - Number(japanese.slice(0, 4)); // wrong: not how eras work ``` Correct: `"japanese"` (and, separately, `"ethiopic"` — see below) is tagged with an era-relative `eraYear` plus an `;era=<name>` tag instead of a plain native year — Temporal's own `.year` for this calendar stays proleptic across era changes, so a plain year would silently misrepresent it. Round-trip through `convertDateToCalendar(value, "gregorian")` instead of computing the ISO year by hand. Source: packages/gmt/src/internal/calendarDateString.ts — see JSDoc for why "japanese" is tagged differently ### MEDIUM Assuming `"ethiopic"`'s annotated year is a fixed offset like `"ethiopic-amete-alem"`/`"coptic"` Wrong: ```ts // Assuming "ethiopic" digits are a fixed transform of the ISO year, the way its siblings' // digits are const ethiopic = convertDateToCalendar("2024-10-03", "ethiopic"); // "2017-01-23[u-ca=ethiopic;era=ethiopic]" — "2017" is era-relative, not a fixed offset const wrongGuess = Number(ethiopic.slice(0, 4)) + 5500; // wrong once the era switches ``` Correct: `"ethiopic"` resets to a new era (Amete Mihret) at its own epoch (~AD 8) and is tagged with `eraYear` + `;era=<name>`, exactly like `"japanese"`. `"ethiopic-amete-alem"` is the same calendar counted continuously with no era reset, so its year _is_ a fixed +5500ish transform of `"ethiopic"`'s eraYear — but only once you're past the era boundary. Round-trip through `convertDateToCalendar(value, "gregorian")` rather than computing it by hand. Source: packages/gmt/src/internal/ethiopicFamilyCalendar.ts — also documents why this family is computed with GMT-owned arithmetic instead of Temporal's native `"ethiopic"`/`"coptic"` calendar ids (an ICU-version compatibility bug in `@js-temporal/polyfill`) ## References - [Full convert API](references/convert-api.md) - [Unix epoch wikipedia](https://en.wikipedia.org/wiki/Unix_time) - [IANA timezone database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)