UNPKG

@burglekitt/gmt

Version:

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

486 lines (367 loc) 17.7 kB
--- name: format-date-time description: > Format plain date/time values for display with locale and Intl.DateTimeFormat options. Use formatDate, formatTime, formatDateTime for absolute formatting; formatDateRange, formatDateTimeRange for a locale-elided range between two plain values; formatDateToParts, formatDateTimeToParts, formatZonedToParts for locale-ordered { type, value } parts; formatRelativeDate, formatRelativeTime, formatRelativeDateTime for human-friendly relative output ("yesterday", "in 2 hours"); formatCalendar, formatCalendarZoned, formatCalendarUnix, formatCalendarUtc for a relative day label + time-of-day ("tomorrow at 2:30 PM") — distinct from the elapsed-time formatRelative* phrasing. Also getLocaleEraNames, getLocaleMonthNames, getLocaleWeekdayNames, getLocaleMeridiems for standalone locale calendar-name lookups (Luxon Info equivalent). Also formatRfc2822, formatHttp, formatSql, formatRfc3339 for fixed, non-locale interchange grammars (email/HTTP headers, SQL, RFC 3339) — no locale argument. sources: - 'burglekitt/gmt:packages/gmt/src/plain/format/index.ts' - 'burglekitt/gmt:packages/gmt/src/plain/format/formatDateRange.ts' - 'burglekitt/gmt:packages/gmt/src/plain/format/formatDateTimeRange.ts' - 'burglekitt/gmt:packages/gmt/src/zoned/format/formatZonedRange.ts' - 'burglekitt/gmt:packages/gmt/src/plain/format/formatDateToParts.ts' - 'burglekitt/gmt:packages/gmt/src/plain/format/formatDateTimeToParts.ts' - 'burglekitt/gmt:packages/gmt/src/zoned/format/formatZonedToParts.ts' - 'burglekitt/gmt:packages/gmt/src/plain/locale/index.ts' - 'burglekitt/gmt:packages/gmt/src/plain/locale/getLocaleEraNames.ts' - 'burglekitt/gmt:packages/gmt/src/plain/locale/getLocaleMonthNames.ts' - 'burglekitt/gmt:packages/gmt/src/plain/locale/getLocaleWeekdayNames.ts' - 'burglekitt/gmt:packages/gmt/src/plain/locale/getLocaleMeridiems.ts' - 'burglekitt/gmt:packages/gmt/src/zoned/format/formatRfc2822.ts' - 'burglekitt/gmt:packages/gmt/src/utc/format/formatHttp.ts' - 'burglekitt/gmt:packages/gmt/src/plain/format/formatSql.ts' - 'burglekitt/gmt:packages/gmt/src/zoned/format/formatRfc3339.ts' - 'burglekitt/gmt:packages/gmt/src/plain/format/formatCalendar.ts' - 'burglekitt/gmt:packages/gmt/src/zoned/format/formatCalendarZoned.ts' - 'burglekitt/gmt:packages/gmt/src/unix/format/formatCalendarUnix.ts' - 'burglekitt/gmt:packages/gmt/src/utc/format/formatCalendarUtc.ts' - 'burglekitt/gmt:packages/gmt/src/internal/joinDateTimeConnector.ts' metadata: type: core library: '@burglekitt/gmt' library_version: '1.14.1' --- # Format Date/Time Use this skill when you need to format date or time values for display to users. ## Setup ```ts import { formatDate, formatTime, formatDateTime, formatRelativeDate, formatRelativeTime, formatRelativeDateTime, } from "@burglekitt/gmt"; ``` ## Core Patterns ### Format date with default locale ```ts const formatted = formatDate("2024-03-15"); // "3/15/2024" (US format) ``` ### Format date with specific locale ```ts const usDate = formatDate("2024-03-15", "en-US"); // "3/15/2024" const ukDate = formatDate("2024-03-15", "en-GB"); // "15/03/2024" const deDate = formatDate("2024-03-15", "de-DE"); // "15.3.2024" ``` ### Format time with locale ```ts const time = formatTime("14:30:45"); // "2:30:45 PM" const timeUk = formatTime("14:30:45", "en-GB"); // "14:30:45" ``` ### Format datetime with locale ```ts const formatted = formatDateTime("2024-03-15T14:30:45"); // "3/15/2024, 2:30:45 PM" const formattedUk = formatDateTime("2024-03-15T14:30:45", "en-GB"); // "15/03/2024, 14:30:45" ``` ### Format with options ```ts import { formatDate } from "@burglekitt/gmt"; const formatted = formatDate("2024-03-15", "en-US", { year: "numeric", month: "long", day: "numeric" }); // "March 15, 2024" ``` ### Format a plain date/datetime range ```ts import { formatDateRange, formatDateTimeRange } from "@burglekitt/gmt"; formatDateRange("2024-02-03", "2024-02-05", "en-US", { dateStyle: "long" }); // "February 3 – 5, 2024" — Intl elides the shared month/year automatically formatDateRange("2024-11-03", "2025-02-10", "en-US", { dateStyle: "long" }); // "November 3, 2024 – February 10, 2025" — no elision once the year differs formatDateTimeRange( "2024-02-03T09:00:00", "2024-02-03T17:00:00", "en-US", { dateStyle: "long", timeStyle: "short" }, ); // "February 3, 2024, 9:00 AM – 5:00 PM" — same day collapses to one date ``` These are the plain (timezone-free) counterparts of `formatZonedRange` (zoned namespace) — same parameter order (`start, end, locale?, options?`) and option shape, wrapping `Intl.DateTimeFormat.prototype.formatRange` directly instead of joining two separately-formatted strings by hand. Do not swap `start`/`end` before calling — a reversed range still formats (it does not throw or auto-correct), so validate ordering yourself if that matters to the caller. ### Get locale-ordered parts instead of a finished string ```ts import { formatDateToParts, formatDateTimeToParts } from "@burglekitt/gmt"; import { formatZonedToParts } from "@burglekitt/gmt"; formatDateToParts("2024-03-15", "en-US"); // [{ type: "month", value: "3" }, { type: "literal", value: "/" }, // { type: "day", value: "15" }, { type: "literal", value: "/" }, // { type: "year", value: "2024" }] formatDateToParts("2024-03-15", "fr-FR"); // day comes before month — same locale-order guarantee formatDate gives you, // but as parts you can restyle instead of a finished string. // [{ type: "day", value: "15" }, { type: "literal", value: "/" }, // { type: "month", value: "3" }, { type: "literal", value: "/" }, // { type: "year", value: "2024" }] formatZonedToParts( "2024-03-15T14:30:00-04:00[America/New_York]", "en-US", { timeZoneName: "longOffset" }, ); // includes { type: "timeZoneName", value: "GMT-4" } ``` Iterate the returned array in order — do not pick out `.find(p => p.type === "month")` and reassemble parts into a fixed order, since that reintroduces the exact locale-ordering bug `formatToParts` exists to avoid (see Common Mistakes below). ### Format relative date ```ts import { formatRelativeDate } from "@burglekitt/gmt"; const ref = "2024-03-15"; formatRelativeDate("2024-03-12", "en-US", { reference: ref }); // "3 days ago" formatRelativeDate("2024-03-18", "en-US", { reference: ref }); // "in 3 days" formatRelativeDate("2024-03-14", "en-US", { reference: ref }); // "yesterday" formatRelativeDate("2023-03-15", "en-US", { reference: ref }); // "last year" ``` ### Format relative time / datetime ```ts import { formatRelativeTime, formatRelativeDateTime } from "@burglekitt/gmt"; formatRelativeTime("11:30:00", "en-US", { reference: "12:00:00" }); // "30 minutes ago" formatRelativeDateTime("2024-03-15T10:00:00", "en-US", { reference: "2024-03-15T12:00:00", }); // "2 hours ago" ``` ### Relative format options - `reference: string` — required anchor (the same shape as `value`) - `numeric: "auto" | "always"` — default `"auto"`. `"auto"` produces "yesterday"/"tomorrow"; `"always"` forces "1 day ago"/"in 1 day" - `style: "long" | "short" | "narrow"` — default `"long"` - `largestUnit` — override the auto-picked unit (e.g. `"week"` to force "3 weeks ago" instead of "last month") ### Format a relative day label + time-of-day (Moment's `.calendar()`) ```ts import { formatCalendar, formatCalendarZoned } from "@burglekitt/gmt"; const ref = "2026-03-15T09:00:00"; formatCalendar("2026-03-16T14:30:00", "en-US", { reference: ref }); // "tomorrow at 2:30 PM" formatCalendar("2026-03-15T14:30:00", "en-US", { reference: ref }); // "today at 2:30 PM" formatCalendar("2026-03-08T14:30:00", "en-US", { reference: ref }); // "March 8, 2026 at 2:30 PM" — 7 days out, beyond the ±6-day threshold, // falls back to an absolute date + time with no relative wording formatCalendarZoned( "2026-03-16T14:30:00-04:00[America/New_York]", "de-DE", { reference: "2026-03-15T09:00:00-04:00[America/New_York]" }, ); // "morgen um 14:30" — the connector ("um") is the locale's own, read from // Intl's combined date+time pattern, never a hardcoded "at" ``` Unlike `formatRelativeDate`/`formatRelativeDateTime` (which auto-pick a unit and always render an elapsed-time phrase — "in 1 day"), `formatCalendar*` always renders a day-granularity label plus the time-of-day, and switches to an absolute date beyond a fixed ±6-day threshold instead of degrading to "in 9 days". Options: `reference` (default "now"), `timeStyle` ("short" default, "medium", plus "full" on the zoned/unix/utc variants — not on plain `formatCalendar`, since a plain value has no real timezone for "full"'s `timeZoneName` to name; see its JSDoc). ### Standalone locale calendar names (no date value needed) These return locale-formatted calendar names directly — the GMT equivalent of Luxon's `Info` class — without requiring a date value: ```ts import { getLocaleEraNames, getLocaleMonthNames, getLocaleWeekdayNames, getLocaleMeridiems, } from "@burglekitt/gmt"; getLocaleEraNames("en-US"); // ["Before Christ", "Anno Domini"] getLocaleEraNames("ja-JP", "short"); // ["紀元前", "西暦"] getLocaleMonthNames("en-US"); // ["January", "February", ... "December"] getLocaleMonthNames("de-DE", "short"); // ["Jan", "Feb", "Mär", ... "Dez"] getLocaleWeekdayNames("en-US"); // ["Sunday", "Monday", ... "Saturday"] (locale-first-day order) getLocaleWeekdayNames("fr-FR"); // ["lundi", "mardi", ... "dimanche"] getLocaleMeridiems("en-US"); // ["AM", "PM"] getLocaleMeridiems("zh-CN"); // ["上午", "下午"] ``` Options: - `getLocaleEraNames(locale, style?)` — 2-element `[BCE-label, CE-label]` Gregorian era names. `style: "long" | "short" | "narrow"` (default `"long"`). - `getLocaleMonthNames(locale, style?)` — 12 Gregorian month names in calendar order. `style: "long" | "short" | "narrow"` (default `"long"`). - `getLocaleWeekdayNames(locale, style?)` — 7 weekday names in the locale's **first-day order** (Sunday-first for en-US, Monday-first for fr-FR). This matches `getLocaleDayOfWeek`, where index 0 is the locale's first day of the week. - `getLocaleMeridiems(locale)` — `[AM-label, PM-label]`; labels are locale-varying (e.g. `en-GB` `["am","pm"]`, `sv-SE` `["fm","em"]`, `zh-CN` `["上午","下午"]`). All four return `[]` for an invalid BCP 47 locale tag. If a locale has no distinct BCE/CE era names, `getLocaleEraNames` returns both elements as the same string — the sentinel is reserved for invalid input only. ### Named machine formats (email, HTTP, SQL, RFC 3339) These are **fixed, non-locale-adaptive grammars**RFC 5322/RFC 7231 mandate English weekday/month abbreviations regardless of caller locale, by specification. This does not contradict the token-formatter exclusion above (Decision 1): there is no locale-appropriate alternative field order to lose, because the grammar is a constant, not a display choice. None of these four take a `locale` argument. ```ts import { formatRfc2822, formatHttp, formatSql, formatRfc3339 } from "@burglekitt/gmt"; // Email `Date:` headers (RFC 5322 / RFC 2822) — zoned namespace. formatRfc2822("2024-03-15T14:30:00-04:00[America/New_York]"); // "Fri, 15 Mar 2024 14:30:00 -0400" // HTTP headers (RFC 7231 IMF-fixdate) — utc namespace, always GMT. formatHttp("2024-03-15T14:30:00Z"); // "Fri, 15 Mar 2024 14:30:00 GMT" // ANSI SQL / ODBC datetime literal — plain namespace, no time zone. formatSql("2024-03-15T14:30:00"); // "2024-03-15 14:30:00" // Strict RFC 3339 — zoned namespace; strips the bracketed IANA zone // GMT's own zoned strings carry, which RFC 3339 does not permit. formatRfc3339("2024-03-15T14:30:00-04:00[America/New_York]"); // "2024-03-15T14:30:00-04:00" ``` Each has a `parse*` counterpart in the `parse-date-time` skill. ## Locale Matrix and Runtime ICU data See [Full format API](references/format-api.md) for the supported-locale formatting matrix and how host ICU data (full vs. small-icu Node builds) affects locale output. ## Common Mistakes ### HIGH Using Intl.DateTimeFormat directly Wrong: ```ts const formatted = new Intl.DateTimeFormat("en-US").format(new Date("2024-03-15")); ``` Correct: ```ts import { formatDate } from "@burglekitt/gmt"; const formatted = formatDate("2024-03-15", "en-US"); ``` Source: AGENTS.md — Never use JavaScript Date APIs ### MEDIUM Not handling empty string on invalid input Wrong: ```ts const formatted = formatDate("invalid-date"); // Assume formatted is always valid string display(formatted); ``` Correct: ```ts import { formatDate, isValidDate } from "@burglekitt/gmt"; const input = "invalid-date"; if (!isValidDate(input)) { display("Invalid date"); } else { display(formatDate(input)); } ``` Source: packages/gmt/src/plain/format/formatDate.ts — Returns "" on invalid input ### MEDIUM Assuming getLocale* throws or returns English for an invalid locale Wrong: ```ts const names = getLocaleMonthNames("not-a-locale"); // Assume names is non-empty or throws render(names); ``` Correct: ```ts import { getLocaleEraNames, getLocaleMonthNames } from "@burglekitt/gmt"; const names = getLocaleMonthNames("en-US"); const eras = getLocaleEraNames("en-US"); if (names.length === 0 || eras.length === 0) { // invalid BCP 47 tag — fall back or surface an error } ``` Source: packages/gmt/src/plain/locale/getLocaleMonthNames.ts — Returns [] for an invalid locale ### MEDIUM Looking for a token-pattern formatter (there isn't one, deliberately) GMT has `parseDateWithPattern`/`parseDateTimeWithPattern`/`parseTimeWithPattern` for *decoding* a string against a caller-supplied token pattern (e.g. `"MM/dd/yyyy"`), but deliberately has no inverse token *formatter* for producing output. This is not a gap to work around — it's roadmap Decision 1 (`context/roadmap/issues/J.md`): a hard-coded token pattern like `"MM/dd/yyyy"` bakes in US field ordering (month before day) and ships it to every locale, which is exactly the kind of locale bug GMT exists to prevent. Parsing a *known, fixed* producer format is fine because there's no ambiguity about what the input shape is; formatting *for output* by a fixed pattern would impose that same fixed shape on every locale's display. Wrong: ```ts // There is no formatDateWithPattern — don't go looking for one, and don't // hand-roll one by string-splitting/concatenating a formatted value into a // fixed field order. ``` Correct: ```ts import { formatDate } from "@burglekitt/gmt"; // Let Intl.DateTimeFormat pick field order per locale instead of a fixed // pattern string. formatDate("2024-03-15", "en-US"); // "3/15/2024" formatDate("2024-03-15", "de-DE"); // "15.3.2024" ``` `formatDate`/`formatTime`/`formatDateTime` (locale-ordered strings) are the current answer for display. For field-level output control (a custom layout that still needs per-locale field order), use `formatDateToParts` / `formatDateTimeToParts` / `formatZonedToParts` — GMT's substitute for a token formatter (Story J12). Source: context/roadmap/issues/J.md — Decision 1 (token formatter deliberately excluded); packages/gmt/src/plain/parse/parseDateWithPattern.ts — the parsing-only counterpart ### MEDIUM Reassembling `formatToParts` output in a fixed order Wrong: ```ts import { formatDateToParts } from "@burglekitt/gmt"; const parts = formatDateToParts("2024-03-15", locale); const month = parts.find((p) => p.type === "month")?.value; const day = parts.find((p) => p.type === "day")?.value; const year = parts.find((p) => p.type === "year")?.value; // Hard-codes US field order onto every locale — exactly the bug // formatToParts exists to avoid. const display = `${month}/${day}/${year}`; ``` Correct: ```ts import { formatDateToParts } from "@burglekitt/gmt"; const parts = formatDateToParts("2024-03-15", locale); const display = parts.map((p) => p.value).join(""); // Iterate as returned — the locale already ordered the parts. Restyle // individual part values (e.g. wrap { type: "literal" } parts differently) // without changing their relative order. ``` Source: context/roadmap/issues/J12"iterate the array as returned; reassembling parts in a fixed order reintroduces exactly the bug formatToParts exists to avoid" ### MEDIUM Confusing formatCalendar with formatRelativeDateTime They answer different questions and are not interchangeable: - `formatCalendar` (and its `formatCalendarZoned`/`formatCalendarUnix`/ `formatCalendarUtc` siblings) — "what should I show on a schedule/agenda item?" `"Tomorrow at 2:30 PM"`. Always includes the clock time; switches to an absolute date beyond ±6 days. - `formatRelativeDateTime` (and its `formatRelative*` siblings) — "how long ago/from now was this?" `"in 1 day"`. Never includes a clock time; auto-picks the coarsest sensible unit (seconds through years) with no distance threshold. Wrong: ```ts import { formatRelativeDateTime } from "@burglekitt/gmt"; // Building a meeting-list row and reaching for the relative formatter — // it has no time-of-day in its output at all. const row = formatRelativeDateTime(meeting.startsAt); // "in 1 day" ``` Correct: ```ts import { formatCalendar } from "@burglekitt/gmt"; const row = formatCalendar(meeting.startsAt); // "tomorrow at 2:30 PM" ``` Source: context/roadmap/issues/J.md — J15's Common Mistakes requirement; packages/gmt/src/plain/format/formatCalendar.ts ## References - [Full format API](references/format-api.md) - [Intl.DateTimeFormat documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)