temporal-fmt 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -67,9 +67,6 @@ function intlPart(temporal, locale, options, partType) {
67
67
  }
68
68
  var TOKENS = [
69
69
  ["yyyy", (t) => pad(t.year, 4), "year"],
70
- // Negative years break the fixed 2-digit width (-45 % 100 === -45), and
71
- // truncating with Math.abs() would make 45 CE and 45 BCE render the same
72
- // string. Throw instead — use yyyy if you need the sign to survive.
73
70
  ["yy", (t) => {
74
71
  if (t.year < 0) {
75
72
  throw new Error(
@@ -95,10 +92,8 @@ var TOKENS = [
95
92
  ["ss", (t) => pad(t.second, 2), "second"],
96
93
  ["s", (t) => String(t.second), "second"],
97
94
  ["SSS", (t) => pad(t.millisecond, 3), "millisecond"],
98
- // dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific
99
- // some locales render it differently or don't split 12-hour at all. We
100
- // still require .hour on the input either way, since that's what Intl
101
- // needs to compute which period it is.
95
+ // dayPeriod text is locale-specific (AM/PM in en-US, م/ص in ar-EG) but
96
+ // still needs .hour on the input to compute which period it is
102
97
  ["a", (t, locale) => intlPart(t, locale, { hour: "numeric", hour12: true }, "dayPeriod"), "hour"],
103
98
  ["zzz", (t) => t.timeZoneId, "timeZoneId"]
104
99
  ];
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/tokens.ts","../src/tokenize.ts","../src/format.ts"],"sourcesContent":["export { format } from './format.js';\nexport type { TemporalLike, FormatOptions } from './tokens.js';","// Pad a number with leading zeros to `len` digits.\nexport function pad(n: number, len: number): string {\n return String(n).padStart(len, '0');\n}\n\n// Minimal duck-typed shape covering every field we might read off a Temporal\n// object. Not every field exists on every type (PlainDate has no .hour, for\n// example) — callers check for undefined before formatting a token.\n//\n// calendarId / toInstant are optional because a plain object satisfying\n// this interface in a test won't have them, but real Temporal instances\n// always do — intlPart() below relies on calendarId to keep Intl from\n// rejecting non-Gregorian objects, and on toInstant to detect ZonedDateTime.\nexport interface TemporalLike {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n timeZoneId?: string;\n dayOfWeek?: number; // 1 (Mon) - 7 (Sun), per Temporal spec\n calendarId?: string;\n toInstant?: () => unknown;\n}\n\nexport interface FormatOptions {\n /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */\n locale?: string;\n}\n\nexport const DEFAULT_LOCALE = 'en-US';\n\n// Small cache so repeated format() calls with the same (locale, options)\n// pair don't construct a fresh Intl.DateTimeFormat every time — these are\n// somewhat expensive to instantiate and format() may run in a loop (e.g.\n// rendering a table of dates).\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\n// In practice the key space is small — a handful of option shapes (month,\n// weekday, dayPeriod) crossed with however many distinct locales a caller\n// uses — so this is defensive rather than fixing an observed leak. Caps\n// memory for the pathological case of an app looping over many distinct\n// locale strings.\nconst MAX_CACHE_SIZE = 500;\n\nfunction getFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = locale + JSON.stringify(options);\n let formatter = formatterCache.get(key);\n if (formatter) {\n return formatter;\n }\n if (formatterCache.size >= MAX_CACHE_SIZE) {\n // Map preserves insertion order, so this evicts the oldest entry —\n // not true LRU, but good enough to bound growth without extra\n // bookkeeping on every cache hit.\n const oldestKey = formatterCache.keys().next().value;\n if (oldestKey !== undefined) formatterCache.delete(oldestKey);\n }\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n return formatter;\n}\n\n// Reads a single named part (e.g. 'month', 'weekday', 'dayPeriod') out of\n// Intl's formatToParts() output. We ask Intl for exactly one field at a\n// time rather than building a full localized string and slicing it apart —\n// slicing is what breaks under RTL scripts and locales with different\n// field ordering (e.g. year-month-day vs day-month-year).\nfunction intlPart(\n temporal: TemporalLike,\n locale: string,\n options: Intl.DateTimeFormatOptions,\n partType: Intl.DateTimeFormatPartTypes\n): string {\n // Intl.DateTimeFormat.formatToParts() always throws on Temporal.ZonedDateTime\n // specifically — deliberate per spec (see\n // Temporal.ZonedDateTime.prototype.toLocaleString docs), not a bug here.\n // Fix: convert to an Instant and pass the zone through the formatter's own\n // `timeZone` option. Converting to PlainDateTime instead would silently\n // drop the timezone info, which breaks combining e.g. 'MMMM' with 'zzz'.\n const { toInstant, timeZoneId } = temporal;\n const isZoned = typeof toInstant === 'function' && typeof timeZoneId === 'string';\n // Must call as temporal.toInstant(), not the destructured toInstant() —\n // it's a prototype method that reads internal slots off `this`, so\n // calling the bare reference throws \"incompatible receiver undefined\".\n const intlSafeTemporal = isZoned ? temporal.toInstant!() : temporal;\n\n // Intl.DateTimeFormat hard-errors (\"Mismatching Calendars\") if the\n // formatter's resolved calendar doesn't match the Temporal object's own\n // calendar — an 'en-US' formatter defaults to gregory, so feeding it a\n // hebrew- or islamic-calendar PlainDate throws unless the formatter is\n // told which calendar to use. We read the calendar off the object itself\n // rather than guessing from the locale, so whatever calendar the caller's\n // Temporal object carries just works — not only Gregorian.\n //\n // Deliberately NOT passed when calendarId is 'iso8601' (the default for\n // plain Temporal objects nobody explicitly gave a calendar to): passing\n // `calendar: 'iso8601'` explicitly to Intl, combined with a single-field\n // options object like `{ month: 'long' }`, makes formatToParts() return\n // an empty parts array instead of the month — a real quirk in how Intl\n // resolves 'iso8601' with partial options, verified against\n // temporal-polyfill/full. Omitting the option entirely sidesteps it and\n // Intl's own default already matches what an iso8601 object needs.\n const calendar = temporal?.calendarId;\n const formatterOptions: Intl.DateTimeFormatOptions = {\n ...options,\n ...(calendar && calendar !== 'iso8601' ? { calendar } : {}),\n ...(isZoned ? { timeZone: timeZoneId } : {}),\n };\n\n // Real Temporal instances (and the Instant from toInstant() above)\n // satisfy Intl at runtime; TS's lib types just don't model that.\n const formatter = getFormatter(locale, formatterOptions);\n const parts = formatter.formatToParts(intlSafeTemporal as Date | number);\n const part = parts.find((p) => p.type === partType);\n if (!part) {\n throw new Error(\n `temporal-fmt: locale \"${locale}\" produced no \"${partType}\" part for this token. ` +\n `This usually means the Temporal object is missing the field the token needs.`\n );\n }\n return part.value;\n}\n\n// Each token knows how to render itself from a TemporalLike + locale, and\n// which field it depends on (used to validate the input actually has that\n// field before we try to format it).\ntype TokenHandler = (t: TemporalLike, locale: string) => string;\n\n// Longest tokens first — the tokenizer is greedy, so \"yyyy\" must be tried\n// before \"yy\" or it'll never match.\n//\n// Numeric tokens (yyyy, MM, dd, HH, mm, ss, SSS) deliberately always render\n// in Western (0-9) digits regardless of locale, even though Intl could give\n// us locale-native digits (Arabic-Indic, Devanagari, etc. via\n// numberingSystem). Conscious choice, not an oversight: mixing locale\n// numeral systems into our pad()-based width logic is a real rabbit hole\n// (padding \"٣\" to 2 digits isn't the same operation as padding \"3\"), and\n// most consumers parsing these strings back out (logs, APIs, filenames)\n// want predictable ASCII digits. Documented as a known limitation in the\n// README rather than silently guessed at here.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n // Negative years break the fixed 2-digit width (-45 % 100 === -45), and\n // truncating with Math.abs() would make 45 CE and 45 BCE render the same\n // string. Throw instead — use yyyy if you need the sign to survive.\n ['yy', (t) => {\n if (t.year! < 0) {\n throw new Error(\n `temporal-fmt: token \"yy\" doesn't support negative years (got ${t.year}), ` +\n `since truncating to 2 digits would make it indistinguishable from a ` +\n `positive year. Use \"yyyy\" instead.`\n );\n }\n return pad(t.year! % 100, 2);\n }, 'year'],\n ['MMMM', (t, locale) => intlPart(t, locale, { month: 'long' }, 'month'), 'month'],\n ['MMM', (t, locale) => intlPart(t, locale, { month: 'short' }, 'month'), 'month'],\n ['MM', (t) => pad(t.month!, 2), 'month'],\n ['M', (t) => String(t.month!), 'month'],\n ['dd', (t) => pad(t.day!, 2), 'day'],\n ['d', (t) => String(t.day!), 'day'],\n ['EEEE', (t, locale) => intlPart(t, locale, { weekday: 'long' }, 'weekday'), 'dayOfWeek'],\n ['EEE', (t, locale) => intlPart(t, locale, { weekday: 'short' }, 'weekday'), 'dayOfWeek'],\n ['HH', (t) => pad(t.hour!, 2), 'hour'],\n ['H', (t) => String(t.hour!), 'hour'],\n ['hh', (t) => pad(t.hour! % 12 || 12, 2), 'hour'],\n ['h', (t) => String(t.hour! % 12 || 12), 'hour'],\n ['mm', (t) => pad(t.minute!, 2), 'minute'],\n ['m', (t) => String(t.minute!), 'minute'],\n ['ss', (t) => pad(t.second!, 2), 'second'],\n ['s', (t) => String(t.second!), 'second'],\n ['SSS', (t) => pad(t.millisecond!, 3), 'millisecond'],\n // dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific\n // — some locales render it differently or don't split 12-hour at all. We\n // still require .hour on the input either way, since that's what Intl\n // needs to compute which period it is.\n ['a', (t, locale) => intlPart(t, locale, { hour: 'numeric', hour12: true }, 'dayPeriod'), 'hour'],\n ['zzz', (t) => t.timeZoneId!, 'timeZoneId'],\n];","import { TOKENS } from './tokens.js';\n\nexport type Piece =\n | { kind: 'token'; value: string }\n | { kind: 'literal'; value: string };\n\n// Sort once, longest-first, so the greedy scanner below never matches \"M\"\n// when \"MMMM\" was actually there.\nconst SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);\n\n/**\n * Splits a format string like `\"yyyy-MM-dd 'at' HH:mm\"` into a sequence of\n * token and literal pieces. Text inside single quotes is always literal —\n * that's how you escape a token that would otherwise be parsed (e.g. a\n * literal \"d\" in \"3rd\" — write 'rd' in quotes so it isn't read as the day token).\n * A doubled quote ('') anywhere means a literal single quote character —\n * this works both inside an open quoted span (e.g. 'it''s' -> it's) and\n * as a standalone escape outside one (e.g. yyyy'' -> \"2026'\").\n */\nexport function tokenize(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n\n while (i < format.length) {\n const ch = format[i];\n\n if (ch === \"'\") {\n // Doubled quote is always a literal ' — check this before treating\n // the quote as an open-delimiter, or \"''best''\" gets misread as\n // \"open quote, then bare text, then open quote\" instead of two\n // separate escaped-apostrophe literals around plain text.\n if (format[i + 1] === \"'\") {\n appendLiteral(pieces, \"'\");\n i += 2;\n continue;\n }\n\n // Otherwise this opens a quoted literal span. Scan forward, treating\n // any '' we find *inside* the span as an escaped literal quote rather\n // than the closing delimiter.\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < format.length) {\n if (format[j] === \"'\") {\n if (format[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += format[j];\n j += 1;\n }\n\n if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in format string \"${format}\"`);\n }\n\n appendLiteral(pieces, literal);\n i = j;\n continue;\n }\n\n const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n pieces.push({ kind: 'token', value: match });\n i += match.length;\n continue;\n }\n\n // Not a token, not a quote — pass the character through as-is. This is\n // what lets you write \"yyyy-MM-dd\" with bare hyphens instead of quoting them.\n appendLiteral(pieces, ch);\n i += 1;\n }\n\n return pieces;\n}\n\n// Merges onto the previous piece when it's also a literal, so a run of\n// bare characters (e.g. \"---\" between tokens) becomes one piece instead\n// of one allocation per character.\nfunction appendLiteral(pieces: Piece[], value: string): void {\n const last = pieces[pieces.length - 1];\n if (last && last.kind === 'literal') {\n last.value += value;\n } else {\n pieces.push({ kind: 'literal', value });\n }\n}","import { TOKENS, DEFAULT_LOCALE, type TemporalLike, type FormatOptions } from './tokens.js';\nimport { tokenize } from './tokenize.js';\n\nconst HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));\n\n// Format strings are supposed to be short, hand-written literals like\n// \"yyyy-MM-dd\" — there's no legitimate reason for one to be thousands of\n// characters. Guards against an attacker (or a bug) feeding an enormous\n// string through to tokenize()/format(), which would otherwise scale\n// linearly with input length with no upper bound.\nconst MAX_FORMAT_LENGTH = 1000;\n\n/**\n * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime\n * using a date-fns-style token string.\n *\n * @example\n * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // \"2026-08-04\"\n * format(zdt, \"MMM d, yyyy 'at' h:mm a\") // \"Aug 4, 2026 at 3:45 PM\"\n * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // \"août 4, 2026\"\n * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names\n *\n * Numeric fields (yyyy, MM, dd, HH, mm, ss, SSS) always render in Western\n * (0-9) digits regardless of locale — this keeps output predictable for\n * anything parsing the result back out (logs, APIs, filenames). Named\n * fields (MMMM, EEEE, a) are fully localized via Intl.DateTimeFormat,\n * including non-Gregorian calendars if the Temporal object itself carries\n * one (e.g. a PlainDate constructed with a Hebrew or Islamic calendar).\n *\n * Throws if the format string uses a token the input type doesn't support\n * (e.g. 'HH' on a PlainDate, which has no time component) — this is\n * deliberate: silently printing \"undefined\" would be worse than failing loudly.\n */\nexport function format(temporal: TemporalLike, formatStr: string, options: FormatOptions = {}): string {\n if (formatStr.length > MAX_FORMAT_LENGTH) {\n throw new Error(\n `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters ` +\n `(got ${formatStr.length}).`\n );\n }\n\n const locale = options.locale ?? DEFAULT_LOCALE;\n const pieces = tokenize(formatStr);\n let result = '';\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n // Shouldn't happen — tokenize() only emits tokens from TOKENS — but keep\n // TypeScript honest and fail loudly instead of silently.\n if (!handler) {\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\n }\n\n if (temporal[handler.field] === undefined) {\n throw new Error(\n `temporal-fmt: token \"${piece.value}\" requires \"${handler.field}\", ` +\n `which this Temporal object doesn't have. ` +\n `(e.g. PlainDate has no time fields, PlainTime has no date fields)`\n );\n }\n\n result += handler.fn(temporal, locale);\n }\n\n return result;\n}"],"mappings":"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","names":["format"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/tokens.ts","../src/tokenize.ts","../src/format.ts"],"sourcesContent":["export { format } from './format.js';\nexport type { TemporalLike, FormatOptions } from './tokens.js';","export function pad(n: number, len: number): string {\n return String(n).padStart(len, '0');\n}\n\n// Not every field exists on every Temporal type (PlainDate has no .hour,\n// etc). Callers check for undefined before formatting a token.\nexport interface TemporalLike {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n timeZoneId?: string;\n dayOfWeek?: number; // 1 (Mon) - 7 (Sun), per Temporal spec\n calendarId?: string;\n toInstant?: () => unknown;\n}\n\nexport interface FormatOptions {\n /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */\n locale?: string;\n}\n\nexport const DEFAULT_LOCALE = 'en-US';\n\n// Intl.DateTimeFormat is expensive to construct and format() can run in a\n// loop (rendering a table of dates), so cache by (locale, options).\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\nconst MAX_CACHE_SIZE = 500;\n\nfunction getFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = locale + JSON.stringify(options);\n let formatter = formatterCache.get(key);\n if (formatter) {\n return formatter;\n }\n if (formatterCache.size >= MAX_CACHE_SIZE) {\n // not real LRU, just evicts oldest insertion — fine for this key space\n const oldestKey = formatterCache.keys().next().value;\n if (oldestKey !== undefined) formatterCache.delete(oldestKey);\n }\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n return formatter;\n}\n\n// Pulls a single field out of formatToParts() rather than building a full\n// string and slicing it — slicing breaks under RTL and locales with\n// different field ordering.\nfunction intlPart(\n temporal: TemporalLike,\n locale: string,\n options: Intl.DateTimeFormatOptions,\n partType: Intl.DateTimeFormatPartTypes\n): string {\n // formatToParts() throws on ZonedDateTime directly (per spec), so convert\n // to Instant and pass the zone via `timeZone` instead. Don't convert to\n // PlainDateTime — that drops the zone, which breaks 'MMMM' + 'zzz' combos.\n const { toInstant, timeZoneId } = temporal;\n const isZoned = typeof toInstant === 'function' && typeof timeZoneId === 'string';\n // has to be called as temporal.toInstant() — destructuring it off breaks\n // the receiver and throws\n const intlSafeTemporal = isZoned ? temporal.toInstant!() : temporal;\n\n // Intl throws \"Mismatching Calendars\" if the formatter's calendar doesn't\n // match the object's own (e.g. en-US formatter defaults to gregory, but\n // a hebrew/islamic PlainDate needs its own calendar passed through).\n //\n // skip this for iso8601 specifically — passing `calendar: 'iso8601'`\n // explicitly alongside a single-field options object makes formatToParts()\n // come back empty. no idea why, cost me an hour.\n const calendar = temporal?.calendarId;\n const formatterOptions: Intl.DateTimeFormatOptions = {\n ...options,\n ...(calendar && calendar !== 'iso8601' ? { calendar } : {}),\n ...(isZoned ? { timeZone: timeZoneId } : {}),\n };\n\n const formatter = getFormatter(locale, formatterOptions);\n const parts = formatter.formatToParts(intlSafeTemporal as Date | number);\n const part = parts.find((p) => p.type === partType);\n if (!part) {\n throw new Error(\n `temporal-fmt: locale \"${locale}\" produced no \"${partType}\" part for this token. ` +\n `This usually means the Temporal object is missing the field the token needs.`\n );\n }\n return part.value;\n}\n\ntype TokenHandler = (t: TemporalLike, locale: string) => string;\n\n// Longest-first — tokenizer is greedy, \"yyyy\" has to be tried before \"yy\".\n//\n// Numeric tokens always render in ASCII digits, never locale-native\n// (Arabic-Indic, Devanagari, etc). Padding non-ASCII digit strings to a\n// fixed width isn't the same operation as padding \"3\", and most consumers\n// parsing these back out want plain digits anyway.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n ['yy', (t) => {\n // -45 % 100 === -45, so truncating negative years to 2 digits doesn't\n // work and Math.abs() would make 45 CE and 45 BCE render the same.\n if (t.year! < 0) {\n throw new Error(\n `temporal-fmt: token \"yy\" doesn't support negative years (got ${t.year}), ` +\n `since truncating to 2 digits would make it indistinguishable from a ` +\n `positive year. Use \"yyyy\" instead.`\n );\n }\n return pad(t.year! % 100, 2);\n }, 'year'],\n ['MMMM', (t, locale) => intlPart(t, locale, { month: 'long' }, 'month'), 'month'],\n ['MMM', (t, locale) => intlPart(t, locale, { month: 'short' }, 'month'), 'month'],\n ['MM', (t) => pad(t.month!, 2), 'month'],\n ['M', (t) => String(t.month!), 'month'],\n ['dd', (t) => pad(t.day!, 2), 'day'],\n ['d', (t) => String(t.day!), 'day'],\n ['EEEE', (t, locale) => intlPart(t, locale, { weekday: 'long' }, 'weekday'), 'dayOfWeek'],\n ['EEE', (t, locale) => intlPart(t, locale, { weekday: 'short' }, 'weekday'), 'dayOfWeek'],\n ['HH', (t) => pad(t.hour!, 2), 'hour'],\n ['H', (t) => String(t.hour!), 'hour'],\n ['hh', (t) => pad(t.hour! % 12 || 12, 2), 'hour'],\n ['h', (t) => String(t.hour! % 12 || 12), 'hour'],\n ['mm', (t) => pad(t.minute!, 2), 'minute'],\n ['m', (t) => String(t.minute!), 'minute'],\n ['ss', (t) => pad(t.second!, 2), 'second'],\n ['s', (t) => String(t.second!), 'second'],\n ['SSS', (t) => pad(t.millisecond!, 3), 'millisecond'],\n // dayPeriod text is locale-specific (AM/PM in en-US, م/ص in ar-EG) but\n // still needs .hour on the input to compute which period it is\n ['a', (t, locale) => intlPart(t, locale, { hour: 'numeric', hour12: true }, 'dayPeriod'), 'hour'],\n ['zzz', (t) => t.timeZoneId!, 'timeZoneId'],\n];\n","import { TOKENS } from './tokens.js';\n\nexport type Piece =\n | { kind: 'token'; value: string }\n | { kind: 'literal'; value: string };\n\n// longest-first so the greedy scan never matches \"M\" when \"MMMM\" was there\nconst SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);\n\n/**\n * Splits a format string like `\"yyyy-MM-dd 'at' HH:mm\"` into token/literal\n * pieces. Text in single quotes is always literal (e.g. write 'rd' in\n * \"3rd\" so it's not read as the day token). A doubled quote ('') means a\n * literal quote character, both inside a quoted span and standalone.\n */\nexport function tokenize(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n\n while (i < format.length) {\n const ch = format[i];\n\n if (ch === \"'\") {\n // check doubled-quote first or \"''best''\" parses wrong\n if (format[i + 1] === \"'\") {\n appendLiteral(pieces, \"'\");\n i += 2;\n continue;\n }\n\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < format.length) {\n if (format[j] === \"'\") {\n if (format[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += format[j];\n j += 1;\n }\n\n if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in format string \"${format}\"`);\n }\n\n appendLiteral(pieces, literal);\n i = j;\n continue;\n }\n\n const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n pieces.push({ kind: 'token', value: match });\n i += match.length;\n continue;\n }\n\n // not a token or quote — pass through as-is \n appendLiteral(pieces, ch);\n i += 1;\n }\n\n return pieces;\n}\n\n// merges into the previous piece if it's also a literal, so \"---\" is one\n// piece instead of three\nfunction appendLiteral(pieces: Piece[], value: string): void {\n const last = pieces[pieces.length - 1];\n if (last && last.kind === 'literal') {\n last.value += value;\n } else {\n pieces.push({ kind: 'literal', value });\n }\n}\n","import { TOKENS, DEFAULT_LOCALE, type TemporalLike, type FormatOptions } from './tokens.js';\nimport { tokenize } from './tokenize.js';\n\nconst HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));\n\n// format strings are short hand-written literals (\"yyyy-MM-dd\") — cap the\n// length so a bug or bad input can't make tokenize() do unbounded work\nconst MAX_FORMAT_LENGTH = 1000;\n\n/**\n * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime\n * using a date-fns-style token string.\n *\n * @example\n * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // \"2026-08-04\"\n * format(zdt, \"MMM d, yyyy 'at' h:mm a\") // \"Aug 4, 2026 at 3:45 PM\"\n * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // \"août 4, 2026\"\n * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names\n *\n * Numeric fields always render in ASCII digits regardless of locale.\n * Named fields (MMMM, EEEE, a) are fully localized via Intl, including\n * non-Gregorian calendars if the Temporal object carries one.\n *\n * Throws if the format string uses a token the input type doesn't support\n * (e.g. 'HH' on a PlainDate) rather than silently printing \"undefined\".\n */\nexport function format(temporal: TemporalLike, formatStr: string, options: FormatOptions = {}): string {\n if (formatStr.length > MAX_FORMAT_LENGTH) {\n throw new Error(\n `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters ` +\n `(got ${formatStr.length}).`\n );\n }\n\n const locale = options.locale ?? DEFAULT_LOCALE;\n const pieces = tokenize(formatStr);\n let result = '';\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n if (!handler) {\n // shouldn't happen — tokenize() only emits tokens from TOKENS\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\n }\n\n if (temporal[handler.field] === undefined) {\n throw new Error(\n `temporal-fmt: token \"${piece.value}\" requires \"${handler.field}\", ` +\n `which this Temporal object doesn't have. ` +\n `(e.g. PlainDate has no time fields, PlainTime has no date fields)`\n );\n }\n\n result += handler.fn(temporal, locale);\n }\n\n return result;\n}\n"],"mappings":"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","names":["format"]}
package/dist/index.d.cts CHANGED
@@ -26,16 +26,12 @@ interface FormatOptions {
26
26
  * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // "août 4, 2026"
27
27
  * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names
28
28
  *
29
- * Numeric fields (yyyy, MM, dd, HH, mm, ss, SSS) always render in Western
30
- * (0-9) digits regardless of locale this keeps output predictable for
31
- * anything parsing the result back out (logs, APIs, filenames). Named
32
- * fields (MMMM, EEEE, a) are fully localized via Intl.DateTimeFormat,
33
- * including non-Gregorian calendars if the Temporal object itself carries
34
- * one (e.g. a PlainDate constructed with a Hebrew or Islamic calendar).
29
+ * Numeric fields always render in ASCII digits regardless of locale.
30
+ * Named fields (MMMM, EEEE, a) are fully localized via Intl, including
31
+ * non-Gregorian calendars if the Temporal object carries one.
35
32
  *
36
33
  * Throws if the format string uses a token the input type doesn't support
37
- * (e.g. 'HH' on a PlainDate, which has no time component) — this is
38
- * deliberate: silently printing "undefined" would be worse than failing loudly.
34
+ * (e.g. 'HH' on a PlainDate) rather than silently printing "undefined".
39
35
  */
40
36
  declare function format(temporal: TemporalLike, formatStr: string, options?: FormatOptions): string;
41
37
 
package/dist/index.d.ts CHANGED
@@ -26,16 +26,12 @@ interface FormatOptions {
26
26
  * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // "août 4, 2026"
27
27
  * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names
28
28
  *
29
- * Numeric fields (yyyy, MM, dd, HH, mm, ss, SSS) always render in Western
30
- * (0-9) digits regardless of locale this keeps output predictable for
31
- * anything parsing the result back out (logs, APIs, filenames). Named
32
- * fields (MMMM, EEEE, a) are fully localized via Intl.DateTimeFormat,
33
- * including non-Gregorian calendars if the Temporal object itself carries
34
- * one (e.g. a PlainDate constructed with a Hebrew or Islamic calendar).
29
+ * Numeric fields always render in ASCII digits regardless of locale.
30
+ * Named fields (MMMM, EEEE, a) are fully localized via Intl, including
31
+ * non-Gregorian calendars if the Temporal object carries one.
35
32
  *
36
33
  * Throws if the format string uses a token the input type doesn't support
37
- * (e.g. 'HH' on a PlainDate, which has no time component) — this is
38
- * deliberate: silently printing "undefined" would be worse than failing loudly.
34
+ * (e.g. 'HH' on a PlainDate) rather than silently printing "undefined".
39
35
  */
40
36
  declare function format(temporal: TemporalLike, formatStr: string, options?: FormatOptions): string;
41
37
 
package/dist/index.js CHANGED
@@ -41,9 +41,6 @@ function intlPart(temporal, locale, options, partType) {
41
41
  }
42
42
  var TOKENS = [
43
43
  ["yyyy", (t) => pad(t.year, 4), "year"],
44
- // Negative years break the fixed 2-digit width (-45 % 100 === -45), and
45
- // truncating with Math.abs() would make 45 CE and 45 BCE render the same
46
- // string. Throw instead — use yyyy if you need the sign to survive.
47
44
  ["yy", (t) => {
48
45
  if (t.year < 0) {
49
46
  throw new Error(
@@ -69,10 +66,8 @@ var TOKENS = [
69
66
  ["ss", (t) => pad(t.second, 2), "second"],
70
67
  ["s", (t) => String(t.second), "second"],
71
68
  ["SSS", (t) => pad(t.millisecond, 3), "millisecond"],
72
- // dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific
73
- // some locales render it differently or don't split 12-hour at all. We
74
- // still require .hour on the input either way, since that's what Intl
75
- // needs to compute which period it is.
69
+ // dayPeriod text is locale-specific (AM/PM in en-US, م/ص in ar-EG) but
70
+ // still needs .hour on the input to compute which period it is
76
71
  ["a", (t, locale) => intlPart(t, locale, { hour: "numeric", hour12: true }, "dayPeriod"), "hour"],
77
72
  ["zzz", (t) => t.timeZoneId, "timeZoneId"]
78
73
  ];
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tokens.ts","../src/tokenize.ts","../src/format.ts"],"sourcesContent":["// Pad a number with leading zeros to `len` digits.\nexport function pad(n: number, len: number): string {\n return String(n).padStart(len, '0');\n}\n\n// Minimal duck-typed shape covering every field we might read off a Temporal\n// object. Not every field exists on every type (PlainDate has no .hour, for\n// example) — callers check for undefined before formatting a token.\n//\n// calendarId / toInstant are optional because a plain object satisfying\n// this interface in a test won't have them, but real Temporal instances\n// always do — intlPart() below relies on calendarId to keep Intl from\n// rejecting non-Gregorian objects, and on toInstant to detect ZonedDateTime.\nexport interface TemporalLike {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n timeZoneId?: string;\n dayOfWeek?: number; // 1 (Mon) - 7 (Sun), per Temporal spec\n calendarId?: string;\n toInstant?: () => unknown;\n}\n\nexport interface FormatOptions {\n /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */\n locale?: string;\n}\n\nexport const DEFAULT_LOCALE = 'en-US';\n\n// Small cache so repeated format() calls with the same (locale, options)\n// pair don't construct a fresh Intl.DateTimeFormat every time — these are\n// somewhat expensive to instantiate and format() may run in a loop (e.g.\n// rendering a table of dates).\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\n// In practice the key space is small — a handful of option shapes (month,\n// weekday, dayPeriod) crossed with however many distinct locales a caller\n// uses — so this is defensive rather than fixing an observed leak. Caps\n// memory for the pathological case of an app looping over many distinct\n// locale strings.\nconst MAX_CACHE_SIZE = 500;\n\nfunction getFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = locale + JSON.stringify(options);\n let formatter = formatterCache.get(key);\n if (formatter) {\n return formatter;\n }\n if (formatterCache.size >= MAX_CACHE_SIZE) {\n // Map preserves insertion order, so this evicts the oldest entry —\n // not true LRU, but good enough to bound growth without extra\n // bookkeeping on every cache hit.\n const oldestKey = formatterCache.keys().next().value;\n if (oldestKey !== undefined) formatterCache.delete(oldestKey);\n }\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n return formatter;\n}\n\n// Reads a single named part (e.g. 'month', 'weekday', 'dayPeriod') out of\n// Intl's formatToParts() output. We ask Intl for exactly one field at a\n// time rather than building a full localized string and slicing it apart —\n// slicing is what breaks under RTL scripts and locales with different\n// field ordering (e.g. year-month-day vs day-month-year).\nfunction intlPart(\n temporal: TemporalLike,\n locale: string,\n options: Intl.DateTimeFormatOptions,\n partType: Intl.DateTimeFormatPartTypes\n): string {\n // Intl.DateTimeFormat.formatToParts() always throws on Temporal.ZonedDateTime\n // specifically — deliberate per spec (see\n // Temporal.ZonedDateTime.prototype.toLocaleString docs), not a bug here.\n // Fix: convert to an Instant and pass the zone through the formatter's own\n // `timeZone` option. Converting to PlainDateTime instead would silently\n // drop the timezone info, which breaks combining e.g. 'MMMM' with 'zzz'.\n const { toInstant, timeZoneId } = temporal;\n const isZoned = typeof toInstant === 'function' && typeof timeZoneId === 'string';\n // Must call as temporal.toInstant(), not the destructured toInstant() —\n // it's a prototype method that reads internal slots off `this`, so\n // calling the bare reference throws \"incompatible receiver undefined\".\n const intlSafeTemporal = isZoned ? temporal.toInstant!() : temporal;\n\n // Intl.DateTimeFormat hard-errors (\"Mismatching Calendars\") if the\n // formatter's resolved calendar doesn't match the Temporal object's own\n // calendar — an 'en-US' formatter defaults to gregory, so feeding it a\n // hebrew- or islamic-calendar PlainDate throws unless the formatter is\n // told which calendar to use. We read the calendar off the object itself\n // rather than guessing from the locale, so whatever calendar the caller's\n // Temporal object carries just works — not only Gregorian.\n //\n // Deliberately NOT passed when calendarId is 'iso8601' (the default for\n // plain Temporal objects nobody explicitly gave a calendar to): passing\n // `calendar: 'iso8601'` explicitly to Intl, combined with a single-field\n // options object like `{ month: 'long' }`, makes formatToParts() return\n // an empty parts array instead of the month — a real quirk in how Intl\n // resolves 'iso8601' with partial options, verified against\n // temporal-polyfill/full. Omitting the option entirely sidesteps it and\n // Intl's own default already matches what an iso8601 object needs.\n const calendar = temporal?.calendarId;\n const formatterOptions: Intl.DateTimeFormatOptions = {\n ...options,\n ...(calendar && calendar !== 'iso8601' ? { calendar } : {}),\n ...(isZoned ? { timeZone: timeZoneId } : {}),\n };\n\n // Real Temporal instances (and the Instant from toInstant() above)\n // satisfy Intl at runtime; TS's lib types just don't model that.\n const formatter = getFormatter(locale, formatterOptions);\n const parts = formatter.formatToParts(intlSafeTemporal as Date | number);\n const part = parts.find((p) => p.type === partType);\n if (!part) {\n throw new Error(\n `temporal-fmt: locale \"${locale}\" produced no \"${partType}\" part for this token. ` +\n `This usually means the Temporal object is missing the field the token needs.`\n );\n }\n return part.value;\n}\n\n// Each token knows how to render itself from a TemporalLike + locale, and\n// which field it depends on (used to validate the input actually has that\n// field before we try to format it).\ntype TokenHandler = (t: TemporalLike, locale: string) => string;\n\n// Longest tokens first — the tokenizer is greedy, so \"yyyy\" must be tried\n// before \"yy\" or it'll never match.\n//\n// Numeric tokens (yyyy, MM, dd, HH, mm, ss, SSS) deliberately always render\n// in Western (0-9) digits regardless of locale, even though Intl could give\n// us locale-native digits (Arabic-Indic, Devanagari, etc. via\n// numberingSystem). Conscious choice, not an oversight: mixing locale\n// numeral systems into our pad()-based width logic is a real rabbit hole\n// (padding \"٣\" to 2 digits isn't the same operation as padding \"3\"), and\n// most consumers parsing these strings back out (logs, APIs, filenames)\n// want predictable ASCII digits. Documented as a known limitation in the\n// README rather than silently guessed at here.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n // Negative years break the fixed 2-digit width (-45 % 100 === -45), and\n // truncating with Math.abs() would make 45 CE and 45 BCE render the same\n // string. Throw instead — use yyyy if you need the sign to survive.\n ['yy', (t) => {\n if (t.year! < 0) {\n throw new Error(\n `temporal-fmt: token \"yy\" doesn't support negative years (got ${t.year}), ` +\n `since truncating to 2 digits would make it indistinguishable from a ` +\n `positive year. Use \"yyyy\" instead.`\n );\n }\n return pad(t.year! % 100, 2);\n }, 'year'],\n ['MMMM', (t, locale) => intlPart(t, locale, { month: 'long' }, 'month'), 'month'],\n ['MMM', (t, locale) => intlPart(t, locale, { month: 'short' }, 'month'), 'month'],\n ['MM', (t) => pad(t.month!, 2), 'month'],\n ['M', (t) => String(t.month!), 'month'],\n ['dd', (t) => pad(t.day!, 2), 'day'],\n ['d', (t) => String(t.day!), 'day'],\n ['EEEE', (t, locale) => intlPart(t, locale, { weekday: 'long' }, 'weekday'), 'dayOfWeek'],\n ['EEE', (t, locale) => intlPart(t, locale, { weekday: 'short' }, 'weekday'), 'dayOfWeek'],\n ['HH', (t) => pad(t.hour!, 2), 'hour'],\n ['H', (t) => String(t.hour!), 'hour'],\n ['hh', (t) => pad(t.hour! % 12 || 12, 2), 'hour'],\n ['h', (t) => String(t.hour! % 12 || 12), 'hour'],\n ['mm', (t) => pad(t.minute!, 2), 'minute'],\n ['m', (t) => String(t.minute!), 'minute'],\n ['ss', (t) => pad(t.second!, 2), 'second'],\n ['s', (t) => String(t.second!), 'second'],\n ['SSS', (t) => pad(t.millisecond!, 3), 'millisecond'],\n // dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific\n // — some locales render it differently or don't split 12-hour at all. We\n // still require .hour on the input either way, since that's what Intl\n // needs to compute which period it is.\n ['a', (t, locale) => intlPart(t, locale, { hour: 'numeric', hour12: true }, 'dayPeriod'), 'hour'],\n ['zzz', (t) => t.timeZoneId!, 'timeZoneId'],\n];","import { TOKENS } from './tokens.js';\n\nexport type Piece =\n | { kind: 'token'; value: string }\n | { kind: 'literal'; value: string };\n\n// Sort once, longest-first, so the greedy scanner below never matches \"M\"\n// when \"MMMM\" was actually there.\nconst SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);\n\n/**\n * Splits a format string like `\"yyyy-MM-dd 'at' HH:mm\"` into a sequence of\n * token and literal pieces. Text inside single quotes is always literal —\n * that's how you escape a token that would otherwise be parsed (e.g. a\n * literal \"d\" in \"3rd\" — write 'rd' in quotes so it isn't read as the day token).\n * A doubled quote ('') anywhere means a literal single quote character —\n * this works both inside an open quoted span (e.g. 'it''s' -> it's) and\n * as a standalone escape outside one (e.g. yyyy'' -> \"2026'\").\n */\nexport function tokenize(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n\n while (i < format.length) {\n const ch = format[i];\n\n if (ch === \"'\") {\n // Doubled quote is always a literal ' — check this before treating\n // the quote as an open-delimiter, or \"''best''\" gets misread as\n // \"open quote, then bare text, then open quote\" instead of two\n // separate escaped-apostrophe literals around plain text.\n if (format[i + 1] === \"'\") {\n appendLiteral(pieces, \"'\");\n i += 2;\n continue;\n }\n\n // Otherwise this opens a quoted literal span. Scan forward, treating\n // any '' we find *inside* the span as an escaped literal quote rather\n // than the closing delimiter.\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < format.length) {\n if (format[j] === \"'\") {\n if (format[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += format[j];\n j += 1;\n }\n\n if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in format string \"${format}\"`);\n }\n\n appendLiteral(pieces, literal);\n i = j;\n continue;\n }\n\n const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n pieces.push({ kind: 'token', value: match });\n i += match.length;\n continue;\n }\n\n // Not a token, not a quote — pass the character through as-is. This is\n // what lets you write \"yyyy-MM-dd\" with bare hyphens instead of quoting them.\n appendLiteral(pieces, ch);\n i += 1;\n }\n\n return pieces;\n}\n\n// Merges onto the previous piece when it's also a literal, so a run of\n// bare characters (e.g. \"---\" between tokens) becomes one piece instead\n// of one allocation per character.\nfunction appendLiteral(pieces: Piece[], value: string): void {\n const last = pieces[pieces.length - 1];\n if (last && last.kind === 'literal') {\n last.value += value;\n } else {\n pieces.push({ kind: 'literal', value });\n }\n}","import { TOKENS, DEFAULT_LOCALE, type TemporalLike, type FormatOptions } from './tokens.js';\nimport { tokenize } from './tokenize.js';\n\nconst HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));\n\n// Format strings are supposed to be short, hand-written literals like\n// \"yyyy-MM-dd\" — there's no legitimate reason for one to be thousands of\n// characters. Guards against an attacker (or a bug) feeding an enormous\n// string through to tokenize()/format(), which would otherwise scale\n// linearly with input length with no upper bound.\nconst MAX_FORMAT_LENGTH = 1000;\n\n/**\n * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime\n * using a date-fns-style token string.\n *\n * @example\n * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // \"2026-08-04\"\n * format(zdt, \"MMM d, yyyy 'at' h:mm a\") // \"Aug 4, 2026 at 3:45 PM\"\n * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // \"août 4, 2026\"\n * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names\n *\n * Numeric fields (yyyy, MM, dd, HH, mm, ss, SSS) always render in Western\n * (0-9) digits regardless of locale — this keeps output predictable for\n * anything parsing the result back out (logs, APIs, filenames). Named\n * fields (MMMM, EEEE, a) are fully localized via Intl.DateTimeFormat,\n * including non-Gregorian calendars if the Temporal object itself carries\n * one (e.g. a PlainDate constructed with a Hebrew or Islamic calendar).\n *\n * Throws if the format string uses a token the input type doesn't support\n * (e.g. 'HH' on a PlainDate, which has no time component) — this is\n * deliberate: silently printing \"undefined\" would be worse than failing loudly.\n */\nexport function format(temporal: TemporalLike, formatStr: string, options: FormatOptions = {}): string {\n if (formatStr.length > MAX_FORMAT_LENGTH) {\n throw new Error(\n `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters ` +\n `(got ${formatStr.length}).`\n );\n }\n\n const locale = options.locale ?? DEFAULT_LOCALE;\n const pieces = tokenize(formatStr);\n let result = '';\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n // Shouldn't happen — tokenize() only emits tokens from TOKENS — but keep\n // TypeScript honest and fail loudly instead of silently.\n if (!handler) {\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\n }\n\n if (temporal[handler.field] === undefined) {\n throw new Error(\n `temporal-fmt: token \"${piece.value}\" requires \"${handler.field}\", ` +\n `which this Temporal object doesn't have. ` +\n `(e.g. PlainDate has no time fields, PlainTime has no date fields)`\n );\n }\n\n result += handler.fn(temporal, locale);\n }\n\n return result;\n}"],"mappings":"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","names":["format"]}
1
+ {"version":3,"sources":["../src/tokens.ts","../src/tokenize.ts","../src/format.ts"],"sourcesContent":["export function pad(n: number, len: number): string {\n return String(n).padStart(len, '0');\n}\n\n// Not every field exists on every Temporal type (PlainDate has no .hour,\n// etc). Callers check for undefined before formatting a token.\nexport interface TemporalLike {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n timeZoneId?: string;\n dayOfWeek?: number; // 1 (Mon) - 7 (Sun), per Temporal spec\n calendarId?: string;\n toInstant?: () => unknown;\n}\n\nexport interface FormatOptions {\n /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */\n locale?: string;\n}\n\nexport const DEFAULT_LOCALE = 'en-US';\n\n// Intl.DateTimeFormat is expensive to construct and format() can run in a\n// loop (rendering a table of dates), so cache by (locale, options).\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\nconst MAX_CACHE_SIZE = 500;\n\nfunction getFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = locale + JSON.stringify(options);\n let formatter = formatterCache.get(key);\n if (formatter) {\n return formatter;\n }\n if (formatterCache.size >= MAX_CACHE_SIZE) {\n // not real LRU, just evicts oldest insertion — fine for this key space\n const oldestKey = formatterCache.keys().next().value;\n if (oldestKey !== undefined) formatterCache.delete(oldestKey);\n }\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n return formatter;\n}\n\n// Pulls a single field out of formatToParts() rather than building a full\n// string and slicing it — slicing breaks under RTL and locales with\n// different field ordering.\nfunction intlPart(\n temporal: TemporalLike,\n locale: string,\n options: Intl.DateTimeFormatOptions,\n partType: Intl.DateTimeFormatPartTypes\n): string {\n // formatToParts() throws on ZonedDateTime directly (per spec), so convert\n // to Instant and pass the zone via `timeZone` instead. Don't convert to\n // PlainDateTime — that drops the zone, which breaks 'MMMM' + 'zzz' combos.\n const { toInstant, timeZoneId } = temporal;\n const isZoned = typeof toInstant === 'function' && typeof timeZoneId === 'string';\n // has to be called as temporal.toInstant() — destructuring it off breaks\n // the receiver and throws\n const intlSafeTemporal = isZoned ? temporal.toInstant!() : temporal;\n\n // Intl throws \"Mismatching Calendars\" if the formatter's calendar doesn't\n // match the object's own (e.g. en-US formatter defaults to gregory, but\n // a hebrew/islamic PlainDate needs its own calendar passed through).\n //\n // skip this for iso8601 specifically — passing `calendar: 'iso8601'`\n // explicitly alongside a single-field options object makes formatToParts()\n // come back empty. no idea why, cost me an hour.\n const calendar = temporal?.calendarId;\n const formatterOptions: Intl.DateTimeFormatOptions = {\n ...options,\n ...(calendar && calendar !== 'iso8601' ? { calendar } : {}),\n ...(isZoned ? { timeZone: timeZoneId } : {}),\n };\n\n const formatter = getFormatter(locale, formatterOptions);\n const parts = formatter.formatToParts(intlSafeTemporal as Date | number);\n const part = parts.find((p) => p.type === partType);\n if (!part) {\n throw new Error(\n `temporal-fmt: locale \"${locale}\" produced no \"${partType}\" part for this token. ` +\n `This usually means the Temporal object is missing the field the token needs.`\n );\n }\n return part.value;\n}\n\ntype TokenHandler = (t: TemporalLike, locale: string) => string;\n\n// Longest-first — tokenizer is greedy, \"yyyy\" has to be tried before \"yy\".\n//\n// Numeric tokens always render in ASCII digits, never locale-native\n// (Arabic-Indic, Devanagari, etc). Padding non-ASCII digit strings to a\n// fixed width isn't the same operation as padding \"3\", and most consumers\n// parsing these back out want plain digits anyway.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n ['yy', (t) => {\n // -45 % 100 === -45, so truncating negative years to 2 digits doesn't\n // work and Math.abs() would make 45 CE and 45 BCE render the same.\n if (t.year! < 0) {\n throw new Error(\n `temporal-fmt: token \"yy\" doesn't support negative years (got ${t.year}), ` +\n `since truncating to 2 digits would make it indistinguishable from a ` +\n `positive year. Use \"yyyy\" instead.`\n );\n }\n return pad(t.year! % 100, 2);\n }, 'year'],\n ['MMMM', (t, locale) => intlPart(t, locale, { month: 'long' }, 'month'), 'month'],\n ['MMM', (t, locale) => intlPart(t, locale, { month: 'short' }, 'month'), 'month'],\n ['MM', (t) => pad(t.month!, 2), 'month'],\n ['M', (t) => String(t.month!), 'month'],\n ['dd', (t) => pad(t.day!, 2), 'day'],\n ['d', (t) => String(t.day!), 'day'],\n ['EEEE', (t, locale) => intlPart(t, locale, { weekday: 'long' }, 'weekday'), 'dayOfWeek'],\n ['EEE', (t, locale) => intlPart(t, locale, { weekday: 'short' }, 'weekday'), 'dayOfWeek'],\n ['HH', (t) => pad(t.hour!, 2), 'hour'],\n ['H', (t) => String(t.hour!), 'hour'],\n ['hh', (t) => pad(t.hour! % 12 || 12, 2), 'hour'],\n ['h', (t) => String(t.hour! % 12 || 12), 'hour'],\n ['mm', (t) => pad(t.minute!, 2), 'minute'],\n ['m', (t) => String(t.minute!), 'minute'],\n ['ss', (t) => pad(t.second!, 2), 'second'],\n ['s', (t) => String(t.second!), 'second'],\n ['SSS', (t) => pad(t.millisecond!, 3), 'millisecond'],\n // dayPeriod text is locale-specific (AM/PM in en-US, م/ص in ar-EG) but\n // still needs .hour on the input to compute which period it is\n ['a', (t, locale) => intlPart(t, locale, { hour: 'numeric', hour12: true }, 'dayPeriod'), 'hour'],\n ['zzz', (t) => t.timeZoneId!, 'timeZoneId'],\n];\n","import { TOKENS } from './tokens.js';\n\nexport type Piece =\n | { kind: 'token'; value: string }\n | { kind: 'literal'; value: string };\n\n// longest-first so the greedy scan never matches \"M\" when \"MMMM\" was there\nconst SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);\n\n/**\n * Splits a format string like `\"yyyy-MM-dd 'at' HH:mm\"` into token/literal\n * pieces. Text in single quotes is always literal (e.g. write 'rd' in\n * \"3rd\" so it's not read as the day token). A doubled quote ('') means a\n * literal quote character, both inside a quoted span and standalone.\n */\nexport function tokenize(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n\n while (i < format.length) {\n const ch = format[i];\n\n if (ch === \"'\") {\n // check doubled-quote first or \"''best''\" parses wrong\n if (format[i + 1] === \"'\") {\n appendLiteral(pieces, \"'\");\n i += 2;\n continue;\n }\n\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < format.length) {\n if (format[j] === \"'\") {\n if (format[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += format[j];\n j += 1;\n }\n\n if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in format string \"${format}\"`);\n }\n\n appendLiteral(pieces, literal);\n i = j;\n continue;\n }\n\n const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n pieces.push({ kind: 'token', value: match });\n i += match.length;\n continue;\n }\n\n // not a token or quote — pass through as-is \n appendLiteral(pieces, ch);\n i += 1;\n }\n\n return pieces;\n}\n\n// merges into the previous piece if it's also a literal, so \"---\" is one\n// piece instead of three\nfunction appendLiteral(pieces: Piece[], value: string): void {\n const last = pieces[pieces.length - 1];\n if (last && last.kind === 'literal') {\n last.value += value;\n } else {\n pieces.push({ kind: 'literal', value });\n }\n}\n","import { TOKENS, DEFAULT_LOCALE, type TemporalLike, type FormatOptions } from './tokens.js';\nimport { tokenize } from './tokenize.js';\n\nconst HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));\n\n// format strings are short hand-written literals (\"yyyy-MM-dd\") — cap the\n// length so a bug or bad input can't make tokenize() do unbounded work\nconst MAX_FORMAT_LENGTH = 1000;\n\n/**\n * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime\n * using a date-fns-style token string.\n *\n * @example\n * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // \"2026-08-04\"\n * format(zdt, \"MMM d, yyyy 'at' h:mm a\") // \"Aug 4, 2026 at 3:45 PM\"\n * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // \"août 4, 2026\"\n * format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names\n *\n * Numeric fields always render in ASCII digits regardless of locale.\n * Named fields (MMMM, EEEE, a) are fully localized via Intl, including\n * non-Gregorian calendars if the Temporal object carries one.\n *\n * Throws if the format string uses a token the input type doesn't support\n * (e.g. 'HH' on a PlainDate) rather than silently printing \"undefined\".\n */\nexport function format(temporal: TemporalLike, formatStr: string, options: FormatOptions = {}): string {\n if (formatStr.length > MAX_FORMAT_LENGTH) {\n throw new Error(\n `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters ` +\n `(got ${formatStr.length}).`\n );\n }\n\n const locale = options.locale ?? DEFAULT_LOCALE;\n const pieces = tokenize(formatStr);\n let result = '';\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n if (!handler) {\n // shouldn't happen — tokenize() only emits tokens from TOKENS\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\n }\n\n if (temporal[handler.field] === undefined) {\n throw new Error(\n `temporal-fmt: token \"${piece.value}\" requires \"${handler.field}\", ` +\n `which this Temporal object doesn't have. ` +\n `(e.g. PlainDate has no time fields, PlainTime has no date fields)`\n );\n }\n\n result += handler.fn(temporal, locale);\n }\n\n return result;\n}\n"],"mappings":";AAAO,SAAS,IAAI,GAAW,KAAqB;AAClD,SAAO,OAAO,CAAC,EAAE,SAAS,KAAK,GAAG;AACpC;AAuBO,IAAM,iBAAiB;AAI9B,IAAM,iBAAiB,oBAAI,IAAiC;AAC5D,IAAM,iBAAiB;AAEvB,SAAS,aAAa,QAAgB,SAA0D;AAC9F,QAAM,MAAM,SAAS,KAAK,UAAU,OAAO;AAC3C,MAAI,YAAY,eAAe,IAAI,GAAG;AACtC,MAAI,WAAW;AACb,WAAO;AAAA,EACT;AACA,MAAI,eAAe,QAAQ,gBAAgB;AAEzC,UAAM,YAAY,eAAe,KAAK,EAAE,KAAK,EAAE;AAC/C,QAAI,cAAc,OAAW,gBAAe,OAAO,SAAS;AAAA,EAC9D;AACA,cAAY,IAAI,KAAK,eAAe,QAAQ,OAAO;AACnD,iBAAe,IAAI,KAAK,SAAS;AACjC,SAAO;AACT;AAKA,SAAS,SACP,UACA,QACA,SACA,UACQ;AAIR,QAAM,EAAE,WAAW,WAAW,IAAI;AAClC,QAAM,UAAU,OAAO,cAAc,cAAc,OAAO,eAAe;AAGzE,QAAM,mBAAmB,UAAU,SAAS,UAAW,IAAI;AAS3D,QAAM,WAAW,UAAU;AAC3B,QAAM,mBAA+C;AAAA,IACnD,GAAG;AAAA,IACH,GAAI,YAAY,aAAa,YAAY,EAAE,SAAS,IAAI,CAAC;AAAA,IACzD,GAAI,UAAU,EAAE,UAAU,WAAW,IAAI,CAAC;AAAA,EAC5C;AAEA,QAAM,YAAY,aAAa,QAAQ,gBAAgB;AACvD,QAAM,QAAQ,UAAU,cAAc,gBAAiC;AACvE,QAAM,OAAO,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ;AAClD,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR,yBAAyB,MAAM,kBAAkB,QAAQ;AAAA,IAE3D;AAAA,EACF;AACA,SAAO,KAAK;AACd;AAUO,IAAM,SAA4D;AAAA,EACvE,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,MAAO,CAAC,GAAG,MAAM;AAAA,EACvC,CAAC,MAAM,CAAC,MAAM;AAGZ,QAAI,EAAE,OAAQ,GAAG;AACf,YAAM,IAAI;AAAA,QACR,gEAAgE,EAAE,IAAI;AAAA,MAGxE;AAAA,IACF;AACA,WAAO,IAAI,EAAE,OAAQ,KAAK,CAAC;AAAA,EAC7B,GAAG,MAAM;AAAA,EACT,CAAC,QAAQ,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,OAAO,OAAO,GAAG,OAAO,GAAG,OAAO;AAAA,EAChF,CAAC,OAAO,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,OAAO,QAAQ,GAAG,OAAO,GAAG,OAAO;AAAA,EAChF,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,OAAQ,CAAC,GAAG,OAAO;AAAA,EACvC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,KAAM,GAAG,OAAO;AAAA,EACtC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,KAAM,CAAC,GAAG,KAAK;AAAA,EACnC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,GAAI,GAAG,KAAK;AAAA,EAClC,CAAC,QAAQ,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,SAAS,OAAO,GAAG,SAAS,GAAG,WAAW;AAAA,EACxF,CAAC,OAAO,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,SAAS,QAAQ,GAAG,SAAS,GAAG,WAAW;AAAA,EACxF,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,MAAO,CAAC,GAAG,MAAM;AAAA,EACrC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,IAAK,GAAG,MAAM;AAAA,EACpC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,OAAQ,MAAM,IAAI,CAAC,GAAG,MAAM;AAAA,EAChD,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,OAAQ,MAAM,EAAE,GAAG,MAAM;AAAA,EAC/C,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,QAAS,CAAC,GAAG,QAAQ;AAAA,EACzC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,MAAO,GAAG,QAAQ;AAAA,EACxC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,QAAS,CAAC,GAAG,QAAQ;AAAA,EACzC,CAAC,KAAK,CAAC,MAAM,OAAO,EAAE,MAAO,GAAG,QAAQ;AAAA,EACxC,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,aAAc,CAAC,GAAG,aAAa;AAAA;AAAA;AAAA,EAGpD,CAAC,KAAK,CAAC,GAAG,WAAW,SAAS,GAAG,QAAQ,EAAE,MAAM,WAAW,QAAQ,KAAK,GAAG,WAAW,GAAG,MAAM;AAAA,EAChG,CAAC,OAAO,CAAC,MAAM,EAAE,YAAa,YAAY;AAC5C;;;AChIA,IAAM,uBAAuB,OAAO,IAAI,CAAC,CAAC,GAAG,MAAM,GAAG,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AAQnF,SAAS,SAASA,SAAyB;AAChD,QAAM,SAAkB,CAAC;AACzB,MAAI,IAAI;AAER,SAAO,IAAIA,QAAO,QAAQ;AACxB,UAAM,KAAKA,QAAO,CAAC;AAEnB,QAAI,OAAO,KAAK;AAEd,UAAIA,QAAO,IAAI,CAAC,MAAM,KAAK;AACzB,sBAAc,QAAQ,GAAG;AACzB,aAAK;AACL;AAAA,MACF;AAEA,UAAI,IAAI,IAAI;AACZ,UAAI,UAAU;AACd,UAAI,SAAS;AACb,aAAO,IAAIA,QAAO,QAAQ;AACxB,YAAIA,QAAO,CAAC,MAAM,KAAK;AACrB,cAAIA,QAAO,IAAI,CAAC,MAAM,KAAK;AACzB,uBAAW;AACX,iBAAK;AACL;AAAA,UACF;AACA,mBAAS;AACT,eAAK;AACL;AAAA,QACF;AACA,mBAAWA,QAAO,CAAC;AACnB,aAAK;AAAA,MACP;AAEA,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,MAAM,sDAAsDA,OAAM,GAAG;AAAA,MACjF;AAEA,oBAAc,QAAQ,OAAO;AAC7B,UAAI;AACJ;AAAA,IACF;AAEA,UAAM,QAAQ,qBAAqB,KAAK,CAAC,QAAQA,QAAO,WAAW,KAAK,CAAC,CAAC;AAC1E,QAAI,OAAO;AACT,aAAO,KAAK,EAAE,MAAM,SAAS,OAAO,MAAM,CAAC;AAC3C,WAAK,MAAM;AACX;AAAA,IACF;AAGA,kBAAc,QAAQ,EAAE;AACxB,SAAK;AAAA,EACP;AAEA,SAAO;AACT;AAIA,SAAS,cAAc,QAAiB,OAAqB;AAC3D,QAAM,OAAO,OAAO,OAAO,SAAS,CAAC;AACrC,MAAI,QAAQ,KAAK,SAAS,WAAW;AACnC,SAAK,SAAS;AAAA,EAChB,OAAO;AACL,WAAO,KAAK,EAAE,MAAM,WAAW,MAAM,CAAC;AAAA,EACxC;AACF;;;AC9EA,IAAM,mBAAmB,IAAI,IAAI,OAAO,IAAI,CAAC,CAAC,KAAK,IAAI,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,MAAM,CAAC,CAAC,CAAC;AAIvF,IAAM,oBAAoB;AAmBnB,SAAS,OAAO,UAAwB,WAAmB,UAAyB,CAAC,GAAW;AACrG,MAAI,UAAU,SAAS,mBAAmB;AACxC,UAAM,IAAI;AAAA,MACR,yDAAyD,iBAAiB,oBAClE,UAAU,MAAM;AAAA,IAC1B;AAAA,EACF;AAEA,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,SAAS,SAAS,SAAS;AACjC,MAAI,SAAS;AAEb,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,SAAS,WAAW;AAC5B,gBAAU,MAAM;AAChB;AAAA,IACF;AAEA,UAAM,UAAU,iBAAiB,IAAI,MAAM,KAAK;AAChD,QAAI,CAAC,SAAS;AAEZ,YAAM,IAAI,MAAM,gCAAgC,MAAM,KAAK,GAAG;AAAA,IAChE;AAEA,QAAI,SAAS,QAAQ,KAAK,MAAM,QAAW;AACzC,YAAM,IAAI;AAAA,QACR,wBAAwB,MAAM,KAAK,eAAe,QAAQ,KAAK;AAAA,MAGjE;AAAA,IACF;AAEA,cAAU,QAAQ,GAAG,UAAU,MAAM;AAAA,EACvC;AAEA,SAAO;AACT;","names":["format"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "temporal-fmt",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Format Temporal.PlainDate/PlainDateTime/PlainTime/ZonedDateTime objects using date-fns-style token strings.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",