temporal-fmt 0.1.1 → 0.2.0
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/README.md +64 -13
- package/dist/index.cjs +42 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -4
- package/dist/index.d.ts +19 -4
- package/dist/index.js +42 -24
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
Format `Temporal.PlainDate` / `PlainTime` / `PlainDateTime` / `ZonedDateTime` objects
|
|
4
4
|
using date-fns-style token strings.
|
|
5
5
|
|
|
6
|
-
Node 26 shipped native `Temporal
|
|
7
|
-
formatter
|
|
8
|
-
|
|
9
|
-
date-fns, moment, or dayjs.
|
|
6
|
+
Node 26 shipped native `Temporal` — and then pointedly left out a custom-string
|
|
7
|
+
formatter. TC39's take: use `Intl.DateTimeFormat` and leave string-token syntax
|
|
8
|
+
to userland. Fair enough, but if you've spent years typing `'yyyy-MM-dd'` out of
|
|
9
|
+
muscle memory from date-fns, moment, or dayjs, that's a rough adjustment. This
|
|
10
|
+
library exists so you don't have to make it.
|
|
10
11
|
|
|
11
|
-
Zero dependencies. You'll need a global `Temporal`
|
|
12
|
-
your own polyfill
|
|
12
|
+
Zero dependencies. You'll need a global `Temporal` — native on Node 26+, or
|
|
13
|
+
bring your own polyfill (`temporal-polyfill` works fine).
|
|
13
14
|
|
|
14
15
|
## Install
|
|
15
16
|
|
|
@@ -33,8 +34,45 @@ const zdt = Temporal.ZonedDateTime.from('2026-08-04T15:45:30-04:00[America/New_Y
|
|
|
33
34
|
format(zdt, 'yyyy-MM-dd HH:mm zzz'); // "2026-08-04 15:45 America/New_York"
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
Wrap literal text in single quotes, like `'at'` above. Need an actual single
|
|
38
|
+
quote in your output? Use `''`.
|
|
39
|
+
|
|
40
|
+
## Locale support
|
|
41
|
+
|
|
42
|
+
Pass a BCP 47 locale tag as a third argument and month names, weekday names,
|
|
43
|
+
and AM/PM markers all localize accordingly. Defaults to `'en-US'` if you don't.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
format(date, 'MMMM d, yyyy', { locale: 'fr-FR' }); // "août 4, 2026"
|
|
47
|
+
format(date, 'EEEE d MMMM', { locale: 'ar-EG' }); // Arabic weekday/month names
|
|
48
|
+
format(dt, 'h:mm a', { locale: 'ja-JP' }); // "3:45 午後"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The named fields (`MMMM`, `MMM`, `EEEE`, `EEE`, `a`) go through
|
|
52
|
+
`Intl.DateTimeFormat` under the hood, which means non-Gregorian calendars
|
|
53
|
+
work too, as long as the `Temporal` object is already carrying one:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const hebrewDate = date.withCalendar('hebrew');
|
|
57
|
+
format(hebrewDate, 'MMMM d, yyyy'); // "Av 21, 5786"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Numeric fields (`yyyy`, `MM`, `dd`, `HH`, `mm`, `ss`, `SSS`) always come out
|
|
61
|
+
in Western (0-9) digits, no matter what locale you pass.** That's on purpose,
|
|
62
|
+
not an oversight. Most things reading this output back in — logs, APIs,
|
|
63
|
+
filenames — want boring, predictable ASCII digits. And honestly, locale-native
|
|
64
|
+
numeral systems like Arabic-Indic or Devanagari don't play nicely with this
|
|
65
|
+
library's own zero-padding logic anyway. If you actually need localized
|
|
66
|
+
digits, run the numeric pieces through `Intl.NumberFormat` yourself.
|
|
67
|
+
|
|
68
|
+
**One more catch: this needs native `Intl`/`Temporal` interop to work.** On
|
|
69
|
+
Node 26+ with native `Temporal`, you're fine. On older Node with a userland
|
|
70
|
+
polyfill, locale-aware tokens will throw — unless you swap in the polyfill's
|
|
71
|
+
own `Intl` export in place of the global one. Why? Because `Intl.DateTimeFormat`
|
|
72
|
+
can't read fields off a non-native `Temporal` object; you'll get a
|
|
73
|
+
`Cannot use valueOf` error for your trouble. That's a limitation baked into how
|
|
74
|
+
`Intl` and `Temporal` currently talk to each other, not something this library
|
|
75
|
+
can paper over.
|
|
38
76
|
|
|
39
77
|
## Tokens
|
|
40
78
|
|
|
@@ -62,15 +100,28 @@ literal single quote.
|
|
|
62
100
|
| a | AM/PM | PM |
|
|
63
101
|
| zzz | IANA time zone id | America/New_York |
|
|
64
102
|
|
|
65
|
-
|
|
66
|
-
you get a
|
|
103
|
+
Try to use a token your input type doesn't support — `HH` on a `PlainDate`,
|
|
104
|
+
say — and you'll get a real error telling you so, not a silent `undefined`
|
|
105
|
+
sitting in your output waiting to confuse someone in three weeks.
|
|
106
|
+
|
|
107
|
+
## Known limitations
|
|
108
|
+
|
|
109
|
+
- Numeral systems are always Western digits — see [Locale support](#locale-support).
|
|
110
|
+
- Requires native `Temporal`/`Intl` interop (Node 26+) for locale-aware tokens.
|
|
67
111
|
|
|
68
112
|
## Dev notes
|
|
69
113
|
|
|
70
114
|
`tsconfig.json` sets `ignoreDeprecations: "6.0"` to work around a tsup bug
|
|
71
|
-
(tsup#1388/#1389)
|
|
72
|
-
|
|
73
|
-
upstream.
|
|
115
|
+
(tsup#1388/#1389) — tsup's dts build step quietly injects a deprecated
|
|
116
|
+
`baseUrl`, and TypeScript 6+ hard-errors on it. This is a workaround, not a
|
|
117
|
+
fix; drop it the moment tsup ships a real one upstream.
|
|
118
|
+
|
|
119
|
+
Tests pull from `temporal-polyfill/full`, not the slim `temporal-polyfill`,
|
|
120
|
+
because the Hebrew-calendar test needs the full build's calendar data — the
|
|
121
|
+
slim one won't cut it. If you're on Node < 26 without native `Temporal`,
|
|
122
|
+
expect the locale-aware tests to fail with `Cannot use valueOf`. That's the
|
|
123
|
+
same polyfill/`Intl` interop gap mentioned above, not a bug in the tests
|
|
124
|
+
themselves. Everything passes clean on Node 26+.
|
|
74
125
|
|
|
75
126
|
## License
|
|
76
127
|
|
package/dist/index.cjs
CHANGED
|
@@ -28,34 +28,47 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
28
|
function pad(n, len) {
|
|
29
29
|
return String(n).padStart(len, "0");
|
|
30
30
|
}
|
|
31
|
-
var
|
|
32
|
-
var
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
var DEFAULT_LOCALE = "en-US";
|
|
32
|
+
var formatterCache = /* @__PURE__ */ new Map();
|
|
33
|
+
function getFormatter(locale, options) {
|
|
34
|
+
const key = locale + JSON.stringify(options);
|
|
35
|
+
let formatter = formatterCache.get(key);
|
|
36
|
+
if (!formatter) {
|
|
37
|
+
formatter = new Intl.DateTimeFormat(locale, options);
|
|
38
|
+
formatterCache.set(key, formatter);
|
|
39
|
+
}
|
|
40
|
+
return formatter;
|
|
41
|
+
}
|
|
42
|
+
function intlPart(temporal, locale, options, partType) {
|
|
43
|
+
const isZoned = typeof temporal?.toInstant === "function" && typeof temporal?.timeZoneId === "string";
|
|
44
|
+
const intlSafeTemporal = isZoned ? temporal.toInstant() : temporal;
|
|
45
|
+
const calendar = temporal?.calendarId;
|
|
46
|
+
const formatterOptions = {
|
|
47
|
+
...options,
|
|
48
|
+
...calendar && calendar !== "iso8601" ? { calendar } : {},
|
|
49
|
+
...isZoned ? { timeZone: temporal.timeZoneId } : {}
|
|
50
|
+
};
|
|
51
|
+
const formatter = getFormatter(locale, formatterOptions);
|
|
52
|
+
const parts = formatter.formatToParts(intlSafeTemporal);
|
|
53
|
+
const part = parts.find((p) => p.type === partType);
|
|
54
|
+
if (!part) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`temporal-fmt: locale "${locale}" produced no "${partType}" part for this token. This usually means the Temporal object is missing the field the token needs.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return part.value;
|
|
60
|
+
}
|
|
48
61
|
var TOKENS = [
|
|
49
62
|
["yyyy", (t) => pad(t.year, 4), "year"],
|
|
50
63
|
["yy", (t) => pad(t.year % 100, 2), "year"],
|
|
51
|
-
["MMMM", (t) =>
|
|
52
|
-
["MMM", (t) =>
|
|
64
|
+
["MMMM", (t, locale) => intlPart(t, locale, { month: "long" }, "month"), "month"],
|
|
65
|
+
["MMM", (t, locale) => intlPart(t, locale, { month: "short" }, "month"), "month"],
|
|
53
66
|
["MM", (t) => pad(t.month, 2), "month"],
|
|
54
67
|
["M", (t) => String(t.month), "month"],
|
|
55
68
|
["dd", (t) => pad(t.day, 2), "day"],
|
|
56
69
|
["d", (t) => String(t.day), "day"],
|
|
57
|
-
["EEEE", (t) =>
|
|
58
|
-
["EEE", (t) =>
|
|
70
|
+
["EEEE", (t, locale) => intlPart(t, locale, { weekday: "long" }, "weekday"), "dayOfWeek"],
|
|
71
|
+
["EEE", (t, locale) => intlPart(t, locale, { weekday: "short" }, "weekday"), "dayOfWeek"],
|
|
59
72
|
["HH", (t) => pad(t.hour, 2), "hour"],
|
|
60
73
|
["H", (t) => String(t.hour), "hour"],
|
|
61
74
|
["hh", (t) => pad(t.hour % 12 || 12, 2), "hour"],
|
|
@@ -65,7 +78,11 @@ var TOKENS = [
|
|
|
65
78
|
["ss", (t) => pad(t.second, 2), "second"],
|
|
66
79
|
["s", (t) => String(t.second), "second"],
|
|
67
80
|
["SSS", (t) => pad(t.millisecond, 3), "millisecond"],
|
|
68
|
-
|
|
81
|
+
// dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific
|
|
82
|
+
// — some locales render it differently or don't split 12-hour at all. We
|
|
83
|
+
// still require .hour on the input either way, since that's what Intl
|
|
84
|
+
// needs to compute which period it is.
|
|
85
|
+
["a", (t, locale) => intlPart(t, locale, { hour: "numeric", hour12: true }, "dayPeriod"), "hour"],
|
|
69
86
|
["zzz", (t) => t.timeZoneId, "timeZoneId"]
|
|
70
87
|
];
|
|
71
88
|
|
|
@@ -120,7 +137,8 @@ function tokenize(format2) {
|
|
|
120
137
|
|
|
121
138
|
// src/format.ts
|
|
122
139
|
var HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));
|
|
123
|
-
function format(temporal, formatStr) {
|
|
140
|
+
function format(temporal, formatStr, options = {}) {
|
|
141
|
+
const locale = options.locale ?? DEFAULT_LOCALE;
|
|
124
142
|
const pieces = tokenize(formatStr);
|
|
125
143
|
let result = "";
|
|
126
144
|
for (const piece of pieces) {
|
|
@@ -137,7 +155,7 @@ function format(temporal, formatStr) {
|
|
|
137
155
|
`temporal-fmt: token "${piece.value}" requires "${handler.field}", which this Temporal object doesn't have. (e.g. PlainDate has no time fields, PlainTime has no date fields)`
|
|
138
156
|
);
|
|
139
157
|
}
|
|
140
|
-
result += handler.fn(temporal);
|
|
158
|
+
result += handler.fn(temporal, locale);
|
|
141
159
|
}
|
|
142
160
|
return result;
|
|
143
161
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -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 } from './tokens.js';\n","// 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.\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}\n\nconst MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\nconst MONTHS_LONG = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\nconst DAYS_SHORT = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\nconst DAYS_LONG = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];\n\n// Each token renders itself from a TemporalLike. The third tuple element\n// below names the field it depends on, so format.ts can check for undefined\n// before calling the handler (e.g. `HH` needs `.hour`, which PlainDate lacks).\ntype TokenHandler = (t: TemporalLike) => string;\n\n// Longest tokens first — the tokenizer is greedy, so \"yyyy\" must be tried\n// before \"yy\" or it'll never match.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n ['yy', (t) => pad(t.year! % 100, 2), 'year'],\n ['MMMM', (t) => MONTHS_LONG[t.month! - 1], 'month'],\n ['MMM', (t) => MONTHS_SHORT[t.month! - 1], '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) => DAYS_LONG[t.dayOfWeek! - 1], 'dayOfWeek'],\n ['EEE', (t) => DAYS_SHORT[t.dayOfWeek! - 1], '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 ['a', (t) => (t.hour! < 12 ? 'AM' : 'PM'), '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 pieces.push({ kind: 'literal', value: \"'\" });\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 pieces.push({ kind: 'literal', value: 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 pieces.push({ kind: 'literal', value: ch });\n i += 1;\n }\n\n return pieces;\n}","import { TOKENS, type TemporalLike } 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/**\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 *\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): string {\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);\n }\n\n return result;\n}\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';","// 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\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 formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\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).\n//\n// `temporal: any` — deliberately untyped. Real Temporal.PlainDate /\n// PlainDateTime / ZonedDateTime instances (not the TemporalLike interface)\n// are what get passed through to Intl at runtime.\nfunction intlPart(\n temporal: any,\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 isZoned = typeof temporal?.toInstant === 'function' && typeof temporal?.timeZoneId === 'string';\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: temporal.timeZoneId } : {}),\n };\n\n const formatter = getFormatter(locale, formatterOptions);\n const parts = formatter.formatToParts(intlSafeTemporal);\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 ['yy', (t) => pad(t.year! % 100, 2), '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 pieces.push({ kind: 'literal', value: \"'\" });\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 pieces.push({ kind: 'literal', value: 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 pieces.push({ kind: 'literal', value: ch });\n i += 1;\n }\n\n return pieces;\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/**\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 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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -8,6 +8,12 @@ interface TemporalLike {
|
|
|
8
8
|
millisecond?: number;
|
|
9
9
|
timeZoneId?: string;
|
|
10
10
|
dayOfWeek?: number;
|
|
11
|
+
calendarId?: string;
|
|
12
|
+
toInstant?: () => unknown;
|
|
13
|
+
}
|
|
14
|
+
interface FormatOptions {
|
|
15
|
+
/** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */
|
|
16
|
+
locale?: string;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
/**
|
|
@@ -15,13 +21,22 @@ interface TemporalLike {
|
|
|
15
21
|
* using a date-fns-style token string.
|
|
16
22
|
*
|
|
17
23
|
* @example
|
|
18
|
-
* format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd')
|
|
19
|
-
* format(zdt, "MMM d, yyyy 'at' h:mm a")
|
|
24
|
+
* format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // "2026-08-04"
|
|
25
|
+
* format(zdt, "MMM d, yyyy 'at' h:mm a") // "Aug 4, 2026 at 3:45 PM"
|
|
26
|
+
* format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // "août 4, 2026"
|
|
27
|
+
* format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names
|
|
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).
|
|
20
35
|
*
|
|
21
36
|
* Throws if the format string uses a token the input type doesn't support
|
|
22
37
|
* (e.g. 'HH' on a PlainDate, which has no time component) — this is
|
|
23
38
|
* deliberate: silently printing "undefined" would be worse than failing loudly.
|
|
24
39
|
*/
|
|
25
|
-
declare function format(temporal: TemporalLike, formatStr: string): string;
|
|
40
|
+
declare function format(temporal: TemporalLike, formatStr: string, options?: FormatOptions): string;
|
|
26
41
|
|
|
27
|
-
export { type TemporalLike, format };
|
|
42
|
+
export { type FormatOptions, type TemporalLike, format };
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ interface TemporalLike {
|
|
|
8
8
|
millisecond?: number;
|
|
9
9
|
timeZoneId?: string;
|
|
10
10
|
dayOfWeek?: number;
|
|
11
|
+
calendarId?: string;
|
|
12
|
+
toInstant?: () => unknown;
|
|
13
|
+
}
|
|
14
|
+
interface FormatOptions {
|
|
15
|
+
/** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */
|
|
16
|
+
locale?: string;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
/**
|
|
@@ -15,13 +21,22 @@ interface TemporalLike {
|
|
|
15
21
|
* using a date-fns-style token string.
|
|
16
22
|
*
|
|
17
23
|
* @example
|
|
18
|
-
* format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd')
|
|
19
|
-
* format(zdt, "MMM d, yyyy 'at' h:mm a")
|
|
24
|
+
* format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // "2026-08-04"
|
|
25
|
+
* format(zdt, "MMM d, yyyy 'at' h:mm a") // "Aug 4, 2026 at 3:45 PM"
|
|
26
|
+
* format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // "août 4, 2026"
|
|
27
|
+
* format(zdt, 'EEEE d MMMM', { locale: 'ar-EG' }) // Arabic weekday/month names
|
|
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).
|
|
20
35
|
*
|
|
21
36
|
* Throws if the format string uses a token the input type doesn't support
|
|
22
37
|
* (e.g. 'HH' on a PlainDate, which has no time component) — this is
|
|
23
38
|
* deliberate: silently printing "undefined" would be worse than failing loudly.
|
|
24
39
|
*/
|
|
25
|
-
declare function format(temporal: TemporalLike, formatStr: string): string;
|
|
40
|
+
declare function format(temporal: TemporalLike, formatStr: string, options?: FormatOptions): string;
|
|
26
41
|
|
|
27
|
-
export { type TemporalLike, format };
|
|
42
|
+
export { type FormatOptions, type TemporalLike, format };
|
package/dist/index.js
CHANGED
|
@@ -2,34 +2,47 @@
|
|
|
2
2
|
function pad(n, len) {
|
|
3
3
|
return String(n).padStart(len, "0");
|
|
4
4
|
}
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
var DEFAULT_LOCALE = "en-US";
|
|
6
|
+
var formatterCache = /* @__PURE__ */ new Map();
|
|
7
|
+
function getFormatter(locale, options) {
|
|
8
|
+
const key = locale + JSON.stringify(options);
|
|
9
|
+
let formatter = formatterCache.get(key);
|
|
10
|
+
if (!formatter) {
|
|
11
|
+
formatter = new Intl.DateTimeFormat(locale, options);
|
|
12
|
+
formatterCache.set(key, formatter);
|
|
13
|
+
}
|
|
14
|
+
return formatter;
|
|
15
|
+
}
|
|
16
|
+
function intlPart(temporal, locale, options, partType) {
|
|
17
|
+
const isZoned = typeof temporal?.toInstant === "function" && typeof temporal?.timeZoneId === "string";
|
|
18
|
+
const intlSafeTemporal = isZoned ? temporal.toInstant() : temporal;
|
|
19
|
+
const calendar = temporal?.calendarId;
|
|
20
|
+
const formatterOptions = {
|
|
21
|
+
...options,
|
|
22
|
+
...calendar && calendar !== "iso8601" ? { calendar } : {},
|
|
23
|
+
...isZoned ? { timeZone: temporal.timeZoneId } : {}
|
|
24
|
+
};
|
|
25
|
+
const formatter = getFormatter(locale, formatterOptions);
|
|
26
|
+
const parts = formatter.formatToParts(intlSafeTemporal);
|
|
27
|
+
const part = parts.find((p) => p.type === partType);
|
|
28
|
+
if (!part) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`temporal-fmt: locale "${locale}" produced no "${partType}" part for this token. This usually means the Temporal object is missing the field the token needs.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return part.value;
|
|
34
|
+
}
|
|
22
35
|
var TOKENS = [
|
|
23
36
|
["yyyy", (t) => pad(t.year, 4), "year"],
|
|
24
37
|
["yy", (t) => pad(t.year % 100, 2), "year"],
|
|
25
|
-
["MMMM", (t) =>
|
|
26
|
-
["MMM", (t) =>
|
|
38
|
+
["MMMM", (t, locale) => intlPart(t, locale, { month: "long" }, "month"), "month"],
|
|
39
|
+
["MMM", (t, locale) => intlPart(t, locale, { month: "short" }, "month"), "month"],
|
|
27
40
|
["MM", (t) => pad(t.month, 2), "month"],
|
|
28
41
|
["M", (t) => String(t.month), "month"],
|
|
29
42
|
["dd", (t) => pad(t.day, 2), "day"],
|
|
30
43
|
["d", (t) => String(t.day), "day"],
|
|
31
|
-
["EEEE", (t) =>
|
|
32
|
-
["EEE", (t) =>
|
|
44
|
+
["EEEE", (t, locale) => intlPart(t, locale, { weekday: "long" }, "weekday"), "dayOfWeek"],
|
|
45
|
+
["EEE", (t, locale) => intlPart(t, locale, { weekday: "short" }, "weekday"), "dayOfWeek"],
|
|
33
46
|
["HH", (t) => pad(t.hour, 2), "hour"],
|
|
34
47
|
["H", (t) => String(t.hour), "hour"],
|
|
35
48
|
["hh", (t) => pad(t.hour % 12 || 12, 2), "hour"],
|
|
@@ -39,7 +52,11 @@ var TOKENS = [
|
|
|
39
52
|
["ss", (t) => pad(t.second, 2), "second"],
|
|
40
53
|
["s", (t) => String(t.second), "second"],
|
|
41
54
|
["SSS", (t) => pad(t.millisecond, 3), "millisecond"],
|
|
42
|
-
|
|
55
|
+
// dayPeriod ('AM'/'PM' in en-US, 'م'/'ص' in ar-EG, etc.) is locale-specific
|
|
56
|
+
// — some locales render it differently or don't split 12-hour at all. We
|
|
57
|
+
// still require .hour on the input either way, since that's what Intl
|
|
58
|
+
// needs to compute which period it is.
|
|
59
|
+
["a", (t, locale) => intlPart(t, locale, { hour: "numeric", hour12: true }, "dayPeriod"), "hour"],
|
|
43
60
|
["zzz", (t) => t.timeZoneId, "timeZoneId"]
|
|
44
61
|
];
|
|
45
62
|
|
|
@@ -94,7 +111,8 @@ function tokenize(format2) {
|
|
|
94
111
|
|
|
95
112
|
// src/format.ts
|
|
96
113
|
var HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));
|
|
97
|
-
function format(temporal, formatStr) {
|
|
114
|
+
function format(temporal, formatStr, options = {}) {
|
|
115
|
+
const locale = options.locale ?? DEFAULT_LOCALE;
|
|
98
116
|
const pieces = tokenize(formatStr);
|
|
99
117
|
let result = "";
|
|
100
118
|
for (const piece of pieces) {
|
|
@@ -111,7 +129,7 @@ function format(temporal, formatStr) {
|
|
|
111
129
|
`temporal-fmt: token "${piece.value}" requires "${handler.field}", which this Temporal object doesn't have. (e.g. PlainDate has no time fields, PlainTime has no date fields)`
|
|
112
130
|
);
|
|
113
131
|
}
|
|
114
|
-
result += handler.fn(temporal);
|
|
132
|
+
result += handler.fn(temporal, locale);
|
|
115
133
|
}
|
|
116
134
|
return result;
|
|
117
135
|
}
|
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.\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}\n\nconst MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\nconst MONTHS_LONG = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\nconst DAYS_SHORT = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\nconst DAYS_LONG = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];\n\n// Each token renders itself from a TemporalLike. The third tuple element\n// below names the field it depends on, so format.ts can check for undefined\n// before calling the handler (e.g. `HH` needs `.hour`, which PlainDate lacks).\ntype TokenHandler = (t: TemporalLike) => string;\n\n// Longest tokens first — the tokenizer is greedy, so \"yyyy\" must be tried\n// before \"yy\" or it'll never match.\nexport const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]> = [\n ['yyyy', (t) => pad(t.year!, 4), 'year'],\n ['yy', (t) => pad(t.year! % 100, 2), 'year'],\n ['MMMM', (t) => MONTHS_LONG[t.month! - 1], 'month'],\n ['MMM', (t) => MONTHS_SHORT[t.month! - 1], '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) => DAYS_LONG[t.dayOfWeek! - 1], 'dayOfWeek'],\n ['EEE', (t) => DAYS_SHORT[t.dayOfWeek! - 1], '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 ['a', (t) => (t.hour! < 12 ? 'AM' : 'PM'), '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 pieces.push({ kind: 'literal', value: \"'\" });\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 pieces.push({ kind: 'literal', value: 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 pieces.push({ kind: 'literal', value: ch });\n i += 1;\n }\n\n return pieces;\n}","import { TOKENS, type TemporalLike } 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/**\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 *\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): string {\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);\n }\n\n return result;\n}\n"],"mappings":"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","names":["format"]}
|
|
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\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 formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\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).\n//\n// `temporal: any` — deliberately untyped. Real Temporal.PlainDate /\n// PlainDateTime / ZonedDateTime instances (not the TemporalLike interface)\n// are what get passed through to Intl at runtime.\nfunction intlPart(\n temporal: any,\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 isZoned = typeof temporal?.toInstant === 'function' && typeof temporal?.timeZoneId === 'string';\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: temporal.timeZoneId } : {}),\n };\n\n const formatter = getFormatter(locale, formatterOptions);\n const parts = formatter.formatToParts(intlSafeTemporal);\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 ['yy', (t) => pad(t.year! % 100, 2), '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 pieces.push({ kind: 'literal', value: \"'\" });\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 pieces.push({ kind: 'literal', value: 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 pieces.push({ kind: 'literal', value: ch });\n i += 1;\n }\n\n return pieces;\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/**\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 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "temporal-fmt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -49,5 +49,8 @@
|
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=24"
|
|
52
|
+
},
|
|
53
|
+
"allowScripts": {
|
|
54
|
+
"esbuild@0.28.1": true
|
|
52
55
|
}
|
|
53
56
|
}
|