@burglekitt/gmt
Version:
Temporal-based date and time utilities with timezone support and polyfill integration
486 lines (367 loc) • 17.7 kB
Markdown
---
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: '/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)