temporal-fmt 0.9.2 → 0.9.4

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.
Files changed (103) hide show
  1. package/README.md +480 -9
  2. package/dist/calendarUtils.cjs +259 -4
  3. package/dist/calendarUtils.cjs.map +1 -1
  4. package/dist/calendarUtils.js +33 -1
  5. package/dist/chunk-4N3WCGEY.js +183 -0
  6. package/dist/{chunk-4JWGUR4O.js.map → chunk-4N3WCGEY.js.map} +1 -1
  7. package/dist/chunk-5UTV5VP6.js +984 -0
  8. package/dist/chunk-5UTV5VP6.js.map +1 -0
  9. package/dist/chunk-ACKKWH5Z.js +578 -0
  10. package/dist/{chunk-2JHEV7ZF.js.map → chunk-ACKKWH5Z.js.map} +1 -1
  11. package/dist/chunk-EGUUO6LE.js +439 -0
  12. package/dist/chunk-EGUUO6LE.js.map +1 -0
  13. package/dist/chunk-FBPHNRVW.js +2939 -0
  14. package/dist/chunk-FBPHNRVW.js.map +1 -0
  15. package/dist/chunk-GPB4JHGI.js +65 -0
  16. package/dist/chunk-GPB4JHGI.js.map +1 -0
  17. package/dist/chunk-H7YQX25T.js +170 -0
  18. package/dist/{chunk-J2AQNXF2.js.map → chunk-H7YQX25T.js.map} +1 -1
  19. package/dist/chunk-HXY2GUUM.js +182 -0
  20. package/dist/{chunk-EHLRZULM.js.map → chunk-HXY2GUUM.js.map} +1 -1
  21. package/dist/chunk-L24NPTAJ.js +328 -0
  22. package/dist/chunk-L24NPTAJ.js.map +1 -0
  23. package/dist/chunk-M3GDTHMT.js +169 -0
  24. package/dist/chunk-M3GDTHMT.js.map +1 -0
  25. package/dist/chunk-MKPDLQKD.js +51 -0
  26. package/dist/chunk-MKPDLQKD.js.map +1 -0
  27. package/dist/chunk-TSLPIURC.js +242 -0
  28. package/dist/{chunk-VQXUMFB2.js.map → chunk-TSLPIURC.js.map} +1 -1
  29. package/dist/chunk-UOPAV3WX.js +75 -0
  30. package/dist/chunk-UOPAV3WX.js.map +1 -0
  31. package/dist/chunk-VNIUIQA4.js +398 -0
  32. package/dist/chunk-VNIUIQA4.js.map +1 -0
  33. package/dist/chunk-YEUASGDK.js +9 -0
  34. package/dist/chunk-YEUASGDK.js.map +1 -0
  35. package/dist/chunk-ZQZF5EXW.js +29 -0
  36. package/dist/chunk-ZQZF5EXW.js.map +1 -0
  37. package/dist/config.d.cts +4 -2
  38. package/dist/config.d.ts +4 -2
  39. package/dist/duration.cjs +638 -26
  40. package/dist/duration.cjs.map +1 -1
  41. package/dist/duration.js +28 -1
  42. package/dist/format.cjs +833 -35
  43. package/dist/format.cjs.map +1 -1
  44. package/dist/format.js +20 -1
  45. package/dist/index.cjs +7698 -163
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +108 -24
  48. package/dist/index.d.ts +108 -24
  49. package/dist/index.js +1536 -58
  50. package/dist/index.js.map +1 -1
  51. package/dist/interval.cjs +5480 -35
  52. package/dist/interval.cjs.map +1 -1
  53. package/dist/interval.js +46 -1
  54. package/dist/localeRegistry.cjs +375 -32
  55. package/dist/localeRegistry.cjs.map +1 -1
  56. package/dist/localeRegistry.js +11 -1
  57. package/dist/modApi.d.cts +131 -0
  58. package/dist/modApi.d.ts +131 -0
  59. package/dist/parse.cjs +1797 -70
  60. package/dist/parse.cjs.map +1 -1
  61. package/dist/parse.js +20 -1
  62. package/dist/pattern.d.cts +1 -0
  63. package/dist/pattern.d.ts +1 -0
  64. package/dist/recurrence.cjs +452 -20
  65. package/dist/recurrence.cjs.map +1 -1
  66. package/dist/recurrence.js +19 -1
  67. package/dist/relativeTime.cjs +268 -21
  68. package/dist/relativeTime.cjs.map +1 -1
  69. package/dist/relativeTime.js +14 -1
  70. package/dist/runtime.d.cts +201 -0
  71. package/dist/runtime.d.ts +201 -0
  72. package/dist/timezone.cjs +387 -20
  73. package/dist/timezone.cjs.map +1 -1
  74. package/dist/timezone.js +26 -1
  75. package/package.json +4 -6
  76. package/scripts/cli.mjs +27 -5
  77. package/dist/chunk-2JHEV7ZF.js +0 -7
  78. package/dist/chunk-3MZLTVP3.js +0 -2
  79. package/dist/chunk-3MZLTVP3.js.map +0 -1
  80. package/dist/chunk-4JWGUR4O.js +0 -11
  81. package/dist/chunk-5U5WJ465.js +0 -36
  82. package/dist/chunk-5U5WJ465.js.map +0 -1
  83. package/dist/chunk-C5YESWFT.js +0 -2
  84. package/dist/chunk-C5YESWFT.js.map +0 -1
  85. package/dist/chunk-C6ZZ62ET.js +0 -2
  86. package/dist/chunk-C6ZZ62ET.js.map +0 -1
  87. package/dist/chunk-EHLRZULM.js +0 -5
  88. package/dist/chunk-F4RGUDA3.js +0 -2
  89. package/dist/chunk-F4RGUDA3.js.map +0 -1
  90. package/dist/chunk-J2AQNXF2.js +0 -2
  91. package/dist/chunk-NBXF7V5B.js +0 -2
  92. package/dist/chunk-NBXF7V5B.js.map +0 -1
  93. package/dist/chunk-R52YOKI3.js +0 -7
  94. package/dist/chunk-R52YOKI3.js.map +0 -1
  95. package/dist/chunk-UJR6DHXY.js +0 -2
  96. package/dist/chunk-UJR6DHXY.js.map +0 -1
  97. package/dist/chunk-VQXUMFB2.js +0 -21
  98. package/dist/chunk-XH46JNPK.js +0 -2
  99. package/dist/chunk-XH46JNPK.js.map +0 -1
  100. package/dist/chunk-YB6YAG7E.js +0 -35
  101. package/dist/chunk-YB6YAG7E.js.map +0 -1
  102. package/dist/chunk-ZGMTY52O.js +0 -2
  103. package/dist/chunk-ZGMTY52O.js.map +0 -1
@@ -0,0 +1,984 @@
1
+ import {
2
+ applyParseNumbering,
3
+ tokenize
4
+ } from "./chunk-M3GDTHMT.js";
5
+ import {
6
+ DEFAULT_LOCALE
7
+ } from "./chunk-L24NPTAJ.js";
8
+ import {
9
+ getTemporal
10
+ } from "./chunk-ZQZF5EXW.js";
11
+ import {
12
+ MAX_FORMAT_LENGTH,
13
+ MAX_INPUT_LENGTH
14
+ } from "./chunk-YEUASGDK.js";
15
+ import {
16
+ AmbiguousInputError,
17
+ FormatSyntaxError,
18
+ InvalidDateError,
19
+ InvalidOffsetError,
20
+ InvalidTimeZoneError,
21
+ ParseMismatchError,
22
+ TemporalFmtError,
23
+ UnknownTokenError,
24
+ assertValidLocaleTag,
25
+ canonicalCacheKey,
26
+ getLocaleVocab,
27
+ subscribeToVocabChanges,
28
+ wrapUntypedError
29
+ } from "./chunk-EGUUO6LE.js";
30
+
31
+ // src/pattern.ts
32
+ function escapeRegExp(literal) {
33
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34
+ }
35
+ function alternation(values, caseInsensitive = false) {
36
+ const escaped = values.map(escapeRegExp);
37
+ if (!caseInsensitive) return `(?:${escaped.join("|")})`;
38
+ return `(?:${escaped.map(foldCase).join("|")})`;
39
+ }
40
+ function foldCase(value) {
41
+ return value.replace(/[a-zA-Z]/g, (ch) => `[${ch.toLowerCase()}${ch.toUpperCase()}]`);
42
+ }
43
+ var TIME_ZONE_SHAPE = "(?:UTC|[+-]\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,9})?)?|[A-Za-z_]+(?:[+-]\\d{1,2})?(?:\\/[A-Za-z0-9_+-]+)*)";
44
+ var OFFSET_SHAPES = {
45
+ X: "(?:Z|[+-]\\d{2}(?:\\d{2})?)",
46
+ XX: "(?:Z|[+-]\\d{4})",
47
+ XXX: "(?:Z|[+-]\\d{2}:\\d{2})",
48
+ x: "[+-]\\d{2}(?:\\d{2})?",
49
+ xx: "[+-]\\d{4}",
50
+ xxx: "[+-]\\d{2}:\\d{2}"
51
+ };
52
+ function getTimeZoneFragment() {
53
+ return TIME_ZONE_SHAPE;
54
+ }
55
+ var validZoneSet;
56
+ function getValidZoneSet() {
57
+ if (!validZoneSet) {
58
+ validZoneSet = new Set(Intl.supportedValuesOf("timeZone"));
59
+ validZoneSet.add("UTC");
60
+ }
61
+ return validZoneSet;
62
+ }
63
+ var FIXED_OFFSET_RE = /^[+-]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/;
64
+ function isValidTimeZone(raw) {
65
+ if (FIXED_OFFSET_RE.test(raw) || getValidZoneSet().has(raw)) return true;
66
+ try {
67
+ getTemporal().ZonedDateTime.from({
68
+ year: 2026,
69
+ month: 1,
70
+ day: 1,
71
+ hour: 0,
72
+ minute: 0,
73
+ second: 0,
74
+ timeZone: raw
75
+ });
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+ var NUMERIC_FRAGMENTS = {
82
+ yy: "\\d{2}",
83
+ MM: "(?:0[1-9]|1[0-2])",
84
+ M: "(?:1[0-2]|[1-9])",
85
+ dd: "(?:0[1-9]|[12]\\d|3[01])",
86
+ d: "(?:[12]\\d|3[01]|[1-9])",
87
+ HH: "(?:[01]\\d|2[0-3])",
88
+ H: "(?:1\\d|2[0-3]|[0-9])",
89
+ hh: "(?:0[1-9]|1[0-2])",
90
+ h: "(?:1[0-2]|[1-9])",
91
+ mm: "(?:[0-5]\\d)",
92
+ m: "(?:[1-5]\\d|[0-9])",
93
+ ss: "(?:[0-5]\\d)",
94
+ s: "(?:[1-5]\\d|[0-9])",
95
+ SSSSSSSSS: "\\d{9}",
96
+ SSSSSSSS: "\\d{8}",
97
+ SSSSSSS: "\\d{7}",
98
+ SSSSSS: "\\d{6}",
99
+ SSSSS: "\\d{5}",
100
+ SSSS: "\\d{4}",
101
+ SSS: "\\d{3}",
102
+ SS: "\\d{2}",
103
+ S: "\\d",
104
+ // Q is always a single digit 1-4 (no padding variant, so no width ambiguity
105
+ // with adjacent digit tokens the way M/d/H/m/s have).
106
+ Q: "[1-4]"
107
+ };
108
+ var QQQ_FRAGMENT = "Q[1-4]";
109
+ var FORMAT_ONLY_TOKENS = /* @__PURE__ */ new Set(["do", "ww", "RRRR", "D", "DD", "DDD", "LLLL", "LLL", "cccc", "ccc", "GGGG", "G", "zzzz", "z"]);
110
+ var YYYY_EXACT = "-?\\d{4}";
111
+ var YYYY_EXTENDED = "-?\\d{4,}";
112
+ var Y_FRAGMENT = "-?\\d+";
113
+ var DIGIT_LEADING_TOKENS = /* @__PURE__ */ new Set([
114
+ "yyyy",
115
+ "yy",
116
+ "y",
117
+ "MM",
118
+ "M",
119
+ "dd",
120
+ "d",
121
+ "HH",
122
+ "H",
123
+ "hh",
124
+ "h",
125
+ "mm",
126
+ "m",
127
+ "ss",
128
+ "s",
129
+ "SSSSSSSSS",
130
+ "SSSSSSSS",
131
+ "SSSSSSS",
132
+ "SSSSSS",
133
+ "SSSSS",
134
+ "SSSS",
135
+ "SSS",
136
+ "SS",
137
+ "S"
138
+ ]);
139
+ var UNBOUNDED_WIDTH_TOKENS = /* @__PURE__ */ new Set(["y"]);
140
+ function tokenFragment(token, locale, nextToken) {
141
+ if (token === "yyyy") {
142
+ return nextToken !== void 0 && DIGIT_LEADING_TOKENS.has(nextToken) ? YYYY_EXACT : YYYY_EXTENDED;
143
+ }
144
+ if (token === "y") {
145
+ return Y_FRAGMENT;
146
+ }
147
+ const numeric = NUMERIC_FRAGMENTS[token];
148
+ if (numeric) {
149
+ return numeric;
150
+ }
151
+ if (token === "QQQ") {
152
+ return QQQ_FRAGMENT;
153
+ }
154
+ if (FORMAT_ONLY_TOKENS.has(token)) {
155
+ throw new UnknownTokenError({
156
+ token,
157
+ message: `temporal-fmt: token "${token}" is format-only \u2014 it can't be parsed back into a value. Use a different token in the parse format string (e.g. "d" for "do", "MM" for "ww").`
158
+ });
159
+ }
160
+ const vocab = getLocaleVocab(locale);
161
+ switch (token) {
162
+ case "MMMM":
163
+ return alternation(vocab.monthLong);
164
+ case "MMM":
165
+ return alternation(vocab.monthShort);
166
+ case "EEEE":
167
+ return alternation(vocab.weekdayLong);
168
+ case "EEE":
169
+ return alternation(vocab.weekdayShort);
170
+ // Case-insensitive on purpose: "pm"/"Pm"/"PM" all mean the same thing,
171
+ // and unlike the Md-glue ambiguity elsewhere in this file, there's no
172
+ // second valid reading to guess wrong — so rejecting on case buys no
173
+ // correctness, only friction against real-world data (mixed-case CSV
174
+ // exports, lowercase log timestamps).
175
+ case "a":
176
+ return alternation(vocab.dayPeriod, true);
177
+ case "zzz":
178
+ return getTimeZoneFragment();
179
+ case "X":
180
+ case "XX":
181
+ case "XXX":
182
+ case "x":
183
+ case "xx":
184
+ case "xxx":
185
+ return OFFSET_SHAPES[token];
186
+ /* c8 ignore start @preserve -- defensive guard, not reachable through
187
+ the public API. tokenFragment's only caller (buildCapturingPattern
188
+ in parsePattern.ts) always passes piece.value straight from
189
+ tokenize.ts, which only ever emits strings from tokens.ts's TOKENS
190
+ table. Every token in that table is handled above: the numeric
191
+ ones via NUMERIC_FRAGMENTS, QQQ via QQQ_FRAGMENT, the format-only
192
+ ones via the FORMAT_ONLY_TOKENS check earlier in this function, and
193
+ everything else via one of the switch cases. There's no token
194
+ string that can reach this default without either tokens.ts
195
+ registering something new or tokenize.ts being bypassed, neither of
196
+ which happens on the parse() path. */
197
+ default:
198
+ throw new UnknownTokenError({ token, message: `temporal-fmt: unknown token "${token}"` });
199
+ }
200
+ }
201
+ var UNPADDED_NUMERIC_TOKENS = /* @__PURE__ */ new Set(["M", "d", "H", "h", "m", "s"]);
202
+ var UNPADDED_NUMERIC_RANGES = {
203
+ M: [{ digits: 1, min: 1, max: 9 }, { digits: 2, min: 10, max: 12 }],
204
+ d: [{ digits: 1, min: 1, max: 9 }, { digits: 2, min: 10, max: 31 }],
205
+ H: [{ digits: 1, min: 0, max: 9 }, { digits: 2, min: 10, max: 23 }],
206
+ h: [{ digits: 1, min: 1, max: 9 }, { digits: 2, min: 10, max: 12 }],
207
+ m: [{ digits: 1, min: 0, max: 9 }, { digits: 2, min: 10, max: 59 }],
208
+ s: [{ digits: 1, min: 0, max: 9 }, { digits: 2, min: 10, max: 59 }]
209
+ };
210
+ function enumerateValidSplits(digits, tokens) {
211
+ const memo = /* @__PURE__ */ new Map();
212
+ function solve(tokenIndex, offset) {
213
+ const key = `${tokenIndex}:${offset}`;
214
+ const cached = memo.get(key);
215
+ if (cached) {
216
+ return cached;
217
+ }
218
+ if (tokenIndex === tokens.length) {
219
+ const result = offset === digits.length ? [[]] : [];
220
+ memo.set(key, result);
221
+ return result;
222
+ }
223
+ const token = tokens[tokenIndex];
224
+ const ranges = UNPADDED_NUMERIC_RANGES[token];
225
+ /* c8 ignore start @preserve -- defensive guard, not reachable through
226
+ the public API. enumerateValidSplits's only caller (parse.ts, both
227
+ call sites) passes run.tokens straight from
228
+ pattern.ambiguousRuns, which parsePattern.ts only ever populates
229
+ with tokens already checked against UNPADDED_NUMERIC_TOKENS — the
230
+ exact same key set as UNPADDED_NUMERIC_RANGES. There's no path
231
+ where a token reaches here without having already passed that
232
+ check. */
233
+ if (!ranges) {
234
+ throw new Error(`temporal-fmt: internal error \u2014 "${token}" is not an unpadded numeric token`);
235
+ }
236
+ /* c8 ignore stop @preserve */
237
+ const results = [];
238
+ for (const { digits: width, min, max } of ranges) {
239
+ if (offset + width > digits.length) continue;
240
+ const piece = digits.slice(offset, offset + width);
241
+ if (width === 2 && piece[0] === "0") continue;
242
+ const value = Number(piece);
243
+ if (value < min || value > max) continue;
244
+ for (const restSplit of solve(tokenIndex + 1, offset + width)) {
245
+ results.push([value, ...restSplit]);
246
+ if (results.length === 2) break;
247
+ }
248
+ if (results.length === 2) break;
249
+ }
250
+ memo.set(key, results);
251
+ return results;
252
+ }
253
+ return solve(0, 0);
254
+ }
255
+
256
+ // src/parsePattern.ts
257
+ function escapeRegExp2(literal) {
258
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
259
+ }
260
+ var MAX_AMBIGUITY_BITS = 12;
261
+ function widthChoicesBits(choices) {
262
+ return Math.ceil(Math.log2(Math.max(choices, 1)));
263
+ }
264
+ function isDigitConsumingStart(piece) {
265
+ if (piece === void 0) return false;
266
+ if (piece.kind === "literal") return /^[0-9]/.test(piece.value);
267
+ return DIGIT_LEADING_TOKENS.has(piece.value);
268
+ }
269
+ function buildCapturingPattern(pieces, locale) {
270
+ const groups = [];
271
+ const ambiguousRuns = [];
272
+ let source = "";
273
+ let i = 0;
274
+ let ambiguityBits = 0;
275
+ let currentRun = { names: [], tokens: [] };
276
+ const flushRun = (nextPiece) => {
277
+ if (currentRun.tokens.length === 1) {
278
+ const name = currentRun.names[0];
279
+ const token = currentRun.tokens[0];
280
+ source += `(?<${name}>${tokenFragment(token, locale)})`;
281
+ if (isDigitConsumingStart(nextPiece)) ambiguityBits += 1;
282
+ } else if (currentRun.tokens.length >= 2) {
283
+ const runName = `r${i++}`;
284
+ const tokenCount = currentRun.tokens.length;
285
+ source += `(?<${runName}>\\d{${tokenCount},${tokenCount * 2}})`;
286
+ ambiguousRuns.push({
287
+ groupName: runName,
288
+ groupNames: currentRun.names,
289
+ tokens: currentRun.tokens
290
+ });
291
+ if (isDigitConsumingStart(nextPiece)) {
292
+ ambiguityBits += widthChoicesBits(tokenCount + 1);
293
+ }
294
+ }
295
+ currentRun = { names: [], tokens: [] };
296
+ };
297
+ for (const [idx, piece] of pieces.entries()) {
298
+ if (piece.kind === "literal") {
299
+ flushRun(piece);
300
+ source += escapeRegExp2(piece.value);
301
+ continue;
302
+ }
303
+ if (UNPADDED_NUMERIC_TOKENS.has(piece.value)) {
304
+ const name2 = `g${i++}`;
305
+ groups.push({ name: name2, token: piece.value });
306
+ currentRun.names.push(name2);
307
+ currentRun.tokens.push(piece.value);
308
+ continue;
309
+ }
310
+ flushRun(piece);
311
+ const name = `g${i++}`;
312
+ groups.push({ name, token: piece.value });
313
+ const nextPiece = pieces[idx + 1];
314
+ const nextToken = nextPiece?.kind === "token" ? nextPiece.value : void 0;
315
+ if (UNBOUNDED_WIDTH_TOKENS.has(piece.value) && isDigitConsumingStart(nextPiece)) {
316
+ throw new FormatSyntaxError({
317
+ reason: `token "${piece.value}" has no fixed width \u2014 it can't be placed directly next to another digit-reading token or a literal that starts with a digit, since there's no way to tell where "${piece.value}" ends and the next field begins. Add a non-digit separator (e.g. "-" or " ") after it.`
318
+ });
319
+ }
320
+ if (piece.value === "yyyy" && nextToken === void 0 && isDigitConsumingStart(nextPiece)) {
321
+ source += `(?<${name}>${tokenFragment(piece.value, locale, "M")})`;
322
+ } else {
323
+ source += `(?<${name}>${tokenFragment(piece.value, locale, nextToken)})`;
324
+ }
325
+ }
326
+ flushRun(void 0);
327
+ if (ambiguityBits > MAX_AMBIGUITY_BITS) {
328
+ throw new FormatSyntaxError({
329
+ reason: `format string has too many variable-width numeric tokens glued to digit-consuming neighbors (ambiguity score ${ambiguityBits} > ${MAX_AMBIGUITY_BITS}). This shape makes the regex engine backtrack exponentially on near-miss input. Add a non-digit separator between these tokens (e.g. "-" or " ") or use their padded forms (MM/dd/HH/mm/ss).`
330
+ });
331
+ }
332
+ return { regex: new RegExp(`^(?:${source})$`, "ud"), groups, ambiguousRuns };
333
+ }
334
+
335
+ // src/parse.ts
336
+ var patternCache = /* @__PURE__ */ new Map();
337
+ var MAX_CACHE_SIZE = 500;
338
+ subscribeToVocabChanges(() => {
339
+ patternCache.clear();
340
+ });
341
+ function getPattern(formatStr, locale) {
342
+ const key = JSON.stringify([canonicalCacheKey(locale), formatStr]);
343
+ let pattern = patternCache.get(key);
344
+ if (pattern) {
345
+ return pattern;
346
+ }
347
+ if (patternCache.size >= MAX_CACHE_SIZE) {
348
+ const oldestKey = patternCache.keys().next().value;
349
+ if (oldestKey !== void 0) patternCache.delete(oldestKey);
350
+ }
351
+ pattern = buildCapturingPattern(tokenize(formatStr), locale);
352
+ patternCache.set(key, pattern);
353
+ return pattern;
354
+ }
355
+ var calendarCache = /* @__PURE__ */ new Map();
356
+ var MAX_CALENDAR_CACHE_SIZE = 500;
357
+ function resolveCalendar(locale) {
358
+ assertValidLocaleTag(locale);
359
+ const canonicalLocale = canonicalCacheKey(locale);
360
+ if (calendarCache.has(canonicalLocale)) {
361
+ return calendarCache.get(canonicalLocale);
362
+ }
363
+ if (calendarCache.size >= MAX_CALENDAR_CACHE_SIZE) {
364
+ const oldestKey = calendarCache.keys().next().value;
365
+ if (oldestKey !== void 0) calendarCache.delete(oldestKey);
366
+ }
367
+ let calendar;
368
+ const parts = canonicalLocale.split("-");
369
+ const extensionIndex = parts.indexOf("u");
370
+ const calendarKeyIndex = extensionIndex === -1 ? -1 : parts.indexOf("ca", extensionIndex + 1);
371
+ if (calendarKeyIndex !== -1 && calendarKeyIndex + 1 < parts.length) {
372
+ const resolved = new Intl.DateTimeFormat(canonicalLocale).resolvedOptions().calendar;
373
+ calendar = resolved === "gregory" ? void 0 : resolved;
374
+ }
375
+ calendarCache.set(canonicalLocale, calendar);
376
+ return calendar;
377
+ }
378
+ function parseOffsetString(raw, token) {
379
+ if (raw === "Z") {
380
+ /* c8 ignore start @preserve -- unreachable: lowercase tokens' regex
381
+ (OFFSET_SHAPES in pattern.ts) has no "Z" alternative at all, so
382
+ raw === 'Z' can only ever be reached when token is one of the
383
+ uppercase variants (X/XX/XXX). A lowercase token can't even
384
+ capture "Z" as `raw` in the first place. */
385
+ if (token === "x" || token === "xx" || token === "xxx") {
386
+ throw new Error(
387
+ `temporal-fmt: offset token "${token}" doesn't accept "Z" \u2014 only the uppercase variants (X/XX/XXX) emit "Z" for UTC. Use "+00:00", "+0000", or "+00" depending on the variant's width.`
388
+ );
389
+ }
390
+ /* c8 ignore stop @preserve */
391
+ return "+00:00";
392
+ }
393
+ const sign = raw[0];
394
+ /* c8 ignore start @preserve -- unreachable: raw is a regex-captured
395
+ group from an offset token, and every OFFSET_SHAPES pattern
396
+ (pattern.ts) is anchored to either "Z" or a leading [+-]. raw's
397
+ first character can never be anything else by the time it reaches
398
+ this function. */
399
+ if (sign !== "+" && sign !== "-") {
400
+ throw new Error(`temporal-fmt: offset "${raw}" for token "${token}" doesn't start with "+", "-", or "Z".`);
401
+ }
402
+ /* c8 ignore stop @preserve */
403
+ const body = raw.slice(1);
404
+ let hoursStr;
405
+ let minutesStr;
406
+ if (body.length === 2) {
407
+ /* c8 ignore start @preserve -- unreachable: each offset token's own
408
+ regex shape in OFFSET_SHAPES (pattern.ts) already gates which
409
+ body shapes it can capture. Only X and x ever match a 2-digit
410
+ body — XX/xx/XXX/xxx's regexes can't produce one — so this
411
+ mismatch can never actually fire through parse(). */
412
+ if (token !== "X" && token !== "x") {
413
+ throw new Error(
414
+ `temporal-fmt: offset token "${token}" can't match "${raw}" \u2014 it requires minutes, but "${raw}" has none.`
415
+ );
416
+ }
417
+ /* c8 ignore stop @preserve */
418
+ hoursStr = body;
419
+ minutesStr = "00";
420
+ } else if (body.length === 4) {
421
+ /* c8 ignore start @preserve -- unreachable, same reason as the
422
+ 2-digit case above: XXX/xxx's regex requires a colon, so it can
423
+ never capture a 4-digit no-colon body in the first place. */
424
+ if (token === "XXX" || token === "xxx") {
425
+ throw new Error(
426
+ `temporal-fmt: offset token "${token}" can't match "${raw}" \u2014 it requires a colon between hours and minutes (e.g. "${sign}${body.slice(0, 2)}:${body.slice(2)}").`
427
+ );
428
+ }
429
+ /* c8 ignore stop @preserve */
430
+ hoursStr = body.slice(0, 2);
431
+ minutesStr = body.slice(2, 4);
432
+ } else if (body.length === 5 && body[2] === ":") {
433
+ /* c8 ignore start @preserve -- unreachable, same reason again: only
434
+ XXX/xxx's regex can produce a colon-shaped body; X/x/XX/xx never
435
+ capture one. */
436
+ if (token !== "XXX" && token !== "xxx") {
437
+ throw new Error(
438
+ `temporal-fmt: offset token "${token}" can't match "${raw}" \u2014 it doesn't use a colon (use "${sign}${body.slice(0, 2)}${body.slice(3)}" instead).`
439
+ );
440
+ }
441
+ /* c8 ignore stop @preserve */
442
+ hoursStr = body.slice(0, 2);
443
+ minutesStr = body.slice(3, 5);
444
+ /* c8 ignore start @preserve -- unreachable: every offset token's
445
+ regex only ever produces a body of length 2, length 4, or length 5
446
+ with a colon at index 2 (see OFFSET_SHAPES in pattern.ts) — no
447
+ shape falls outside those three cases, so this else arm can't be
448
+ taken through parse(). Kept as an exhaustiveness fallback so
449
+ hoursStr/minutesStr are assigned on every path TypeScript can see. */
450
+ } else {
451
+ throw new Error(`temporal-fmt: offset "${raw}" doesn't match the shape token "${token}" accepts.`);
452
+ }
453
+ /* c8 ignore stop @preserve */
454
+ const hours = Number(hoursStr);
455
+ const minutes = Number(minutesStr);
456
+ if (hours > 14) {
457
+ throw new InvalidOffsetError({
458
+ actual: raw,
459
+ message: `temporal-fmt: offset hours ${hours} in "${raw}" out of range (max 14 \u2014 Kiritimati, Line Islands is +14:00).`
460
+ });
461
+ }
462
+ if (minutes > 59) {
463
+ throw new InvalidOffsetError({
464
+ actual: raw,
465
+ message: `temporal-fmt: offset minutes ${minutes} in "${raw}" out of range (max 59).`
466
+ });
467
+ }
468
+ if (sign === "+" && hours === 14 && minutes !== 0) {
469
+ throw new InvalidOffsetError({
470
+ actual: raw,
471
+ message: `temporal-fmt: offset "${raw}" exceeds the maximum supported UTC offset of +14:00.`
472
+ });
473
+ }
474
+ if (sign === "-" && hours === 12 && minutes !== 0) {
475
+ throw new InvalidOffsetError({
476
+ actual: raw,
477
+ message: `temporal-fmt: offset "${raw}" exceeds the maximum supported negative UTC offset of -12:00.`
478
+ });
479
+ }
480
+ return `${sign}${hoursStr}:${minutesStr}`;
481
+ }
482
+ function assignField(fields, key, value) {
483
+ fields[key] = value;
484
+ }
485
+ function applyGroup(fields, token, raw, locale, formatStr) {
486
+ const vocab = getLocaleVocab(locale);
487
+ switch (token) {
488
+ case "yyyy":
489
+ case "y":
490
+ assignField(fields, "year", Number(raw));
491
+ break;
492
+ case "yy":
493
+ assignField(fields, "twoDigitYear", Number(raw));
494
+ break;
495
+ case "MM":
496
+ case "M":
497
+ assignField(fields, "month", Number(raw));
498
+ break;
499
+ case "MMMM":
500
+ assignField(fields, "month", vocab.monthLong.indexOf(raw) + 1);
501
+ break;
502
+ case "MMM":
503
+ assignField(fields, "month", vocab.monthShort.indexOf(raw) + 1);
504
+ break;
505
+ case "dd":
506
+ case "d":
507
+ assignField(fields, "day", Number(raw));
508
+ break;
509
+ case "EEEE":
510
+ assignField(fields, "weekdayRaw", raw);
511
+ assignField(fields, "weekdayExpected", vocab.weekdayLong.indexOf(raw) + 1);
512
+ break;
513
+ case "EEE":
514
+ assignField(fields, "weekdayRaw", raw);
515
+ assignField(fields, "weekdayExpected", vocab.weekdayShort.indexOf(raw) + 1);
516
+ break;
517
+ case "HH":
518
+ case "H":
519
+ assignField(fields, "hour", Number(raw));
520
+ break;
521
+ case "hh":
522
+ case "h":
523
+ assignField(fields, "hour12", Number(raw));
524
+ break;
525
+ case "mm":
526
+ case "m":
527
+ assignField(fields, "minute", Number(raw));
528
+ break;
529
+ case "ss":
530
+ case "s":
531
+ assignField(fields, "second", Number(raw));
532
+ break;
533
+ case "S":
534
+ case "SS":
535
+ case "SSS":
536
+ case "SSSS":
537
+ case "SSSSS":
538
+ case "SSSSSS":
539
+ case "SSSSSSS":
540
+ case "SSSSSSSS":
541
+ case "SSSSSSSSS": {
542
+ const nanoOfSecond = Number(raw.padEnd(9, "0"));
543
+ assignField(fields, "millisecond", Math.floor(nanoOfSecond / 1e6));
544
+ assignField(fields, "microsecond", Math.floor(nanoOfSecond / 1e3) % 1e3);
545
+ assignField(fields, "nanosecond", nanoOfSecond % 1e3);
546
+ break;
547
+ }
548
+ case "a": {
549
+ const periodIndex = vocab.dayPeriod.findIndex((p) => p.toLowerCase() === raw.toLowerCase());
550
+ /* c8 ignore start @preserve -- unreachable: the 'a' token's regex
551
+ fragment (pattern.ts's alternation() over vocab.dayPeriod) can
552
+ only ever capture a case-insensitive match of one of
553
+ vocab.dayPeriod's own entries. Both the regex and this lookup
554
+ derive their vocab from the same `locale` via getLocaleVocab(),
555
+ so periodIndex can't come back negative through parse(). */
556
+ if (periodIndex < 0) throw new Error(`temporal-fmt: unknown day period "${raw}" for locale "${locale}".`);
557
+ /* c8 ignore stop @preserve */
558
+ assignField(fields, "dayPeriodRaw", raw);
559
+ assignField(fields, "isPM", periodIndex === 1);
560
+ break;
561
+ }
562
+ case "zzz":
563
+ assignField(fields, "timeZoneId", raw);
564
+ break;
565
+ case "X":
566
+ case "XX":
567
+ case "XXX":
568
+ case "x":
569
+ case "xx":
570
+ case "xxx":
571
+ assignField(fields, "offsetString", parseOffsetString(raw, token));
572
+ break;
573
+ case "Q":
574
+ assignField(fields, "quarter", Number(raw));
575
+ break;
576
+ case "QQQ":
577
+ assignField(fields, "quarter", Number(raw.slice(1)));
578
+ break;
579
+ }
580
+ }
581
+ function pickLenientSplit(splits, tokens) {
582
+ const dayIndex = tokens.indexOf("d");
583
+ if (dayIndex !== -1) {
584
+ const smallDaySplits = splits.filter((s) => s[dayIndex] <= 12);
585
+ if (smallDaySplits.length > 0) {
586
+ return smallDaySplits[0];
587
+ }
588
+ }
589
+ return splits[0];
590
+ }
591
+ function resolveYear(fields) {
592
+ if (fields.year !== void 0 && fields.twoDigitYear !== void 0) {
593
+ throw new FormatSyntaxError({
594
+ message: 'temporal-fmt: format string mixes a full-year token ("yyyy"/"y") with the two-digit "yy" token.'
595
+ });
596
+ }
597
+ if (fields.year !== void 0) return fields.year;
598
+ if (fields.twoDigitYear !== void 0) {
599
+ return fields.twoDigitYear <= 68 ? 2e3 + fields.twoDigitYear : 1900 + fields.twoDigitYear;
600
+ }
601
+ return void 0;
602
+ }
603
+ function resolveHour(fields, formatStr, locale) {
604
+ if (fields.hour !== void 0 && fields.hour12 !== void 0) {
605
+ throw new FormatSyntaxError({
606
+ format: formatStr,
607
+ message: `temporal-fmt: format string "${formatStr}" mixes a 24-hour token ("HH"/"H") with a 12-hour token ("hh"/"h").`
608
+ });
609
+ }
610
+ if (fields.hour !== void 0) {
611
+ if (fields.dayPeriodRaw !== void 0) {
612
+ throw new FormatSyntaxError({
613
+ format: formatStr,
614
+ message: `temporal-fmt: format string "${formatStr}" mixes a 24-hour token ("HH"/"H") with a day-period token ("a"). Both describe the same field; use one or the other, not both.`
615
+ });
616
+ }
617
+ return fields.hour;
618
+ }
619
+ if (fields.hour12 !== void 0) {
620
+ if (fields.isPM === void 0) {
621
+ throw new FormatSyntaxError({
622
+ format: formatStr,
623
+ message: `temporal-fmt: format string "${formatStr}" uses a 12-hour token ("hh"/"h") without an "a" token, so parse() can't tell AM from PM.`
624
+ });
625
+ }
626
+ return fields.hour12 % 12 + (fields.isPM ? 12 : 0);
627
+ }
628
+ return void 0;
629
+ }
630
+ function parse(formatStr, input, options = {}) {
631
+ if (formatStr.length > MAX_FORMAT_LENGTH) {
632
+ throw new FormatSyntaxError({
633
+ format: formatStr,
634
+ message: `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters (got ${formatStr.length}).`
635
+ });
636
+ }
637
+ if (input.length > MAX_INPUT_LENGTH) {
638
+ throw new FormatSyntaxError({
639
+ input,
640
+ message: `temporal-fmt: input exceeds maximum length of ${MAX_INPUT_LENGTH} characters (got ${input.length}).`
641
+ });
642
+ }
643
+ if (options.parseNumberingSystem) {
644
+ input = applyParseNumbering(input, options);
645
+ }
646
+ const locale = options.locale ?? DEFAULT_LOCALE;
647
+ const calendar = resolveCalendar(locale);
648
+ const pattern = getPattern(formatStr, locale);
649
+ const match = pattern.regex.exec(input);
650
+ if (!match) {
651
+ throw new ParseMismatchError({
652
+ input,
653
+ format: formatStr,
654
+ // reason set explicitly (not just message): parse.test.js's
655
+ // "safeParse: failure returns { ok: false, error } with a
656
+ // TemporalFmtError" asserts result.error.reason carries the
657
+ // underlying detail text, matching what wrapUntypedError used to
658
+ // populate when this message reached it as a caught plain Error.
659
+ reason: "no valid pattern matches the format string and input shape",
660
+ message: `temporal-fmt: no valid pattern matches the format string and input shape`
661
+ });
662
+ }
663
+ if (pattern.groups.length === 0) {
664
+ throw new ParseMismatchError({
665
+ format: formatStr,
666
+ // reason set explicitly: errors.test.js's "format string with no
667
+ // tokens at all falls through to ParseMismatchError" asserts
668
+ // error.reason matches /has no tokens/.
669
+ reason: `format string "${formatStr}" has no tokens \u2014 nothing to parse into a value.`,
670
+ message: `temporal-fmt: format string "${formatStr}" has no tokens \u2014 nothing to parse into a value.`
671
+ });
672
+ }
673
+ for (const { name, token } of pattern.groups) {
674
+ if (token === "zzz" && !isValidTimeZone(match.groups[name])) {
675
+ throw new InvalidTimeZoneError({
676
+ input,
677
+ format: formatStr,
678
+ actual: match.groups[name],
679
+ reason: "not a recognized IANA time zone identifier"
680
+ });
681
+ }
682
+ }
683
+ const runValues = /* @__PURE__ */ new Map();
684
+ for (const run of pattern.ambiguousRuns) {
685
+ const runDigits = match.groups[run.groupName];
686
+ const splits = enumerateValidSplits(runDigits, run.tokens);
687
+ if (splits.length === 0) {
688
+ throw new ParseMismatchError({
689
+ input,
690
+ format: formatStr,
691
+ reason: "no valid pattern matches the format string and input shape",
692
+ message: `temporal-fmt: no valid pattern matches the format string and input shape`
693
+ });
694
+ }
695
+ if (splits.length > 1) {
696
+ if (!options.lenient) {
697
+ throw new AmbiguousInputError({
698
+ input,
699
+ format: formatStr,
700
+ message: `temporal-fmt: "${runDigits}" in format string "${formatStr}" is ambiguous \u2014 ${splits.length} different ways to read tokens "${run.tokens.join("")}" (with no separator between them) are all individually valid (e.g. ${JSON.stringify(splits[0])} vs ${JSON.stringify(splits[1])}). parse() won't guess; add a separator between these tokens, or use their padded form (e.g. "MM" instead of "M") so each one has a fixed width. Pass { lenient: true } to opt into a documented heuristic that picks one.`
701
+ });
702
+ }
703
+ const picked = pickLenientSplit(splits, run.tokens);
704
+ run.groupNames.forEach((name, idx) => runValues.set(name, String(picked[idx])));
705
+ } else {
706
+ run.groupNames.forEach((name, idx) => runValues.set(name, String(splits[0][idx])));
707
+ }
708
+ }
709
+ const fields = {};
710
+ for (const { name, token } of pattern.groups) {
711
+ const raw = runValues.get(name) ?? match.groups[name];
712
+ applyGroup(fields, token, raw, locale, formatStr);
713
+ }
714
+ const year = resolveYear(fields);
715
+ const hour = resolveHour(fields, formatStr, locale);
716
+ const { month, day, minute, second, millisecond, microsecond, nanosecond, timeZoneId, offsetString, weekdayExpected, weekdayRaw, quarter } = fields;
717
+ const hasAnyDatePart = year !== void 0 || month !== void 0 || day !== void 0;
718
+ const hasFullDate = year !== void 0 && month !== void 0 && day !== void 0;
719
+ if (hasAnyDatePart && !hasFullDate) {
720
+ throw new InvalidDateError({
721
+ format: formatStr,
722
+ message: `temporal-fmt: format string "${formatStr}" has an incomplete date \u2014 year, month, and day tokens must all be present together.`
723
+ });
724
+ }
725
+ const hasTime = hour !== void 0 || minute !== void 0 || second !== void 0 || millisecond !== void 0;
726
+ if (timeZoneId !== void 0 && !(hasFullDate && hasTime)) {
727
+ throw new FormatSyntaxError({
728
+ format: formatStr,
729
+ message: `temporal-fmt: format string "${formatStr}" has a "zzz" token but needs a full date and time to build a ZonedDateTime.`
730
+ });
731
+ }
732
+ if (offsetString !== void 0 && !(hasFullDate && hasTime)) {
733
+ throw new FormatSyntaxError({
734
+ format: formatStr,
735
+ message: `temporal-fmt: format string "${formatStr}" has an offset token (X/XX/XXX/x/xx/xxx) but needs a full date and time to build a ZonedDateTime.`
736
+ });
737
+ }
738
+ if (weekdayExpected !== void 0 && !hasFullDate) {
739
+ throw new FormatSyntaxError({
740
+ format: formatStr,
741
+ message: `temporal-fmt: format string "${formatStr}" has a weekday token ("EEEE"/"EEE") but needs a full date to validate it against.`
742
+ });
743
+ }
744
+ if (!hasFullDate && !hasTime) {
745
+ throw new FormatSyntaxError({
746
+ format: formatStr,
747
+ message: `temporal-fmt: format string "${formatStr}" has no date or time tokens to parse.`
748
+ });
749
+ }
750
+ const temporal = getTemporal();
751
+ const timeFields = {
752
+ hour: hour ?? 0,
753
+ minute: minute ?? 0,
754
+ second: second ?? 0,
755
+ millisecond: millisecond ?? 0,
756
+ microsecond: microsecond ?? 0,
757
+ nanosecond: nanosecond ?? 0
758
+ };
759
+ const calendarField = calendar ? { calendar } : {};
760
+ const reject = { overflow: "reject" };
761
+ let result;
762
+ try {
763
+ if (timeZoneId !== void 0) {
764
+ const zoneOptions = { overflow: "reject", offset: "prefer" };
765
+ result = temporal.ZonedDateTime.from(
766
+ {
767
+ year,
768
+ month,
769
+ day,
770
+ ...timeFields,
771
+ ...calendarField,
772
+ timeZone: timeZoneId,
773
+ ...offsetString !== void 0 ? { offset: offsetString } : {}
774
+ },
775
+ zoneOptions
776
+ );
777
+ if (offsetString !== void 0) {
778
+ const zdt = result;
779
+ const wallClockShifted = zdt.hour !== timeFields.hour || zdt.minute !== timeFields.minute || zdt.second !== timeFields.second;
780
+ if (wallClockShifted) {
781
+ throw new InvalidDateError({
782
+ input,
783
+ format: formatStr,
784
+ message: `"${timeZoneId}" has no such wall-clock time on this date \u2014 it falls in a DST gap, not an ambiguous or valid instant.`
785
+ });
786
+ }
787
+ const actualOffset = zdt.offset;
788
+ if (actualOffset !== offsetString) {
789
+ throw new ParseMismatchError({
790
+ input,
791
+ format: formatStr,
792
+ message: `has both a "zzz" zone (${timeZoneId}) and an offset token (${offsetString}), but the zone's actual offset at this date/time is ${actualOffset}, not ${offsetString}.`
793
+ });
794
+ }
795
+ }
796
+ } else if (offsetString !== void 0) {
797
+ throw new ParseMismatchError({
798
+ input,
799
+ format: formatStr,
800
+ message: `format string "${formatStr}" has an offset token but no "zzz" zone token. An offset does not identify a time zone by itself \u2014 add "zzz" to the pattern, or parse into a PlainDateTime/PlainDate/PlainTime if a zone isn't needed.`
801
+ });
802
+ } else if (hasFullDate && hasTime) {
803
+ result = temporal.PlainDateTime.from({ year, month, day, ...timeFields, ...calendarField }, reject);
804
+ } else if (hasFullDate) {
805
+ result = temporal.PlainDate.from({ year, month, day, ...calendarField }, reject);
806
+ } else {
807
+ result = temporal.PlainTime.from(timeFields, reject);
808
+ }
809
+ } catch (err) {
810
+ throw new InvalidDateError({
811
+ input,
812
+ format: formatStr,
813
+ message: `temporal-fmt: "${input}" doesn't describe a valid date/time for format "${formatStr}": ${err.message}`
814
+ });
815
+ }
816
+ if (weekdayExpected !== void 0) {
817
+ const actual = result.dayOfWeek;
818
+ if (actual !== weekdayExpected) {
819
+ const vocab = getLocaleVocab(locale);
820
+ throw new InvalidDateError({
821
+ input,
822
+ format: formatStr,
823
+ message: `temporal-fmt: "${weekdayRaw}" doesn't match the actual weekday (${vocab.weekdayLong[actual - 1]}) for the parsed date.`
824
+ });
825
+ }
826
+ }
827
+ if (quarter !== void 0 && month !== void 0) {
828
+ const expectedQuarter = Math.ceil(month / 3);
829
+ if (quarter !== expectedQuarter) {
830
+ throw new InvalidDateError({
831
+ format: formatStr,
832
+ message: `temporal-fmt: format string "${formatStr}" contains a quarter token (Q/QQQ) whose value (Q${quarter}) disagrees with the parsed month's actual quarter \u2014 month ${month} is in Q${expectedQuarter}.`
833
+ });
834
+ }
835
+ }
836
+ return result;
837
+ }
838
+ function safeParse(formatStr, input, options = {}) {
839
+ try {
840
+ return { ok: true, value: parse(formatStr, input, options) };
841
+ } catch (err) {
842
+ /* c8 ignore start @preserve */
843
+ if (err instanceof TemporalFmtError) {
844
+ return { ok: false, error: err };
845
+ }
846
+ return { ok: false, error: wrapUntypedError(err, { input, format: formatStr }) };
847
+ }
848
+ /* c8 ignore stop @preserve */
849
+ }
850
+ function tryParse(formatStr, input, options = {}) {
851
+ try {
852
+ return parse(formatStr, input, options);
853
+ } catch {
854
+ return void 0;
855
+ }
856
+ }
857
+ function parseToParts(formatStr, input, options = {}) {
858
+ if (formatStr.length > MAX_FORMAT_LENGTH) {
859
+ throw new FormatSyntaxError({
860
+ format: formatStr,
861
+ message: `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters (got ${formatStr.length}).`
862
+ });
863
+ }
864
+ if (input.length > MAX_INPUT_LENGTH) {
865
+ throw new FormatSyntaxError({
866
+ input,
867
+ message: `temporal-fmt: input exceeds maximum length of ${MAX_INPUT_LENGTH} characters (got ${input.length}).`
868
+ });
869
+ }
870
+ if (options.parseNumberingSystem) {
871
+ input = applyParseNumbering(input, options);
872
+ }
873
+ const locale = options.locale ?? DEFAULT_LOCALE;
874
+ const pattern = getPattern(formatStr, locale);
875
+ const match = pattern.regex.exec(input);
876
+ if (!match) {
877
+ throw new ParseMismatchError({
878
+ input,
879
+ format: formatStr,
880
+ // reason set explicitly (not just message): parse.test.js's
881
+ // "safeParse: failure returns { ok: false, error } with a
882
+ // TemporalFmtError" asserts result.error.reason carries the
883
+ // underlying detail text, matching what wrapUntypedError used to
884
+ // populate when this message reached it as a caught plain Error.
885
+ reason: "no valid pattern matches the format string and input shape",
886
+ message: `temporal-fmt: no valid pattern matches the format string and input shape`
887
+ });
888
+ }
889
+ if (pattern.groups.length === 0) {
890
+ throw new ParseMismatchError({
891
+ format: formatStr,
892
+ // reason set explicitly: errors.test.js's "format string with no
893
+ // tokens at all falls through to ParseMismatchError" asserts
894
+ // error.reason matches /has no tokens/.
895
+ reason: `format string "${formatStr}" has no tokens \u2014 nothing to parse into a value.`,
896
+ message: `temporal-fmt: format string "${formatStr}" has no tokens \u2014 nothing to parse into a value.`
897
+ });
898
+ }
899
+ for (const { name, token } of pattern.groups) {
900
+ if (token === "zzz" && !isValidTimeZone(match.groups[name])) {
901
+ throw new InvalidTimeZoneError({
902
+ input,
903
+ format: formatStr,
904
+ actual: match.groups[name],
905
+ reason: "not a recognized IANA time zone identifier"
906
+ });
907
+ }
908
+ }
909
+ const runValues = /* @__PURE__ */ new Map();
910
+ for (const run of pattern.ambiguousRuns) {
911
+ const runDigits = match.groups[run.groupName];
912
+ const splits = enumerateValidSplits(runDigits, run.tokens);
913
+ if (splits.length === 0) {
914
+ throw new ParseMismatchError({
915
+ input,
916
+ format: formatStr,
917
+ reason: "no valid pattern matches the format string and input shape",
918
+ message: `temporal-fmt: no valid pattern matches the format string and input shape`
919
+ });
920
+ }
921
+ if (splits.length > 1) {
922
+ if (!options.lenient) {
923
+ throw new AmbiguousInputError({
924
+ input,
925
+ format: formatStr,
926
+ message: `temporal-fmt: "${runDigits}" in format string "${formatStr}" is ambiguous \u2014 ${splits.length} different ways to read tokens "${run.tokens.join("")}" (with no separator between them) are all individually valid (e.g. ${JSON.stringify(splits[0])} vs ${JSON.stringify(splits[1])}). parse() won't guess; add a separator between these tokens, or use their padded form (e.g. "MM" instead of "M") so each one has a fixed width. Pass { lenient: true } to opt into a documented heuristic that picks one.`
927
+ });
928
+ }
929
+ const picked = pickLenientSplit(splits, run.tokens);
930
+ run.groupNames.forEach((name, idx) => runValues.set(name, String(picked[idx])));
931
+ } else {
932
+ run.groupNames.forEach((name, idx) => runValues.set(name, String(splits[0][idx])));
933
+ }
934
+ }
935
+ const parts = [];
936
+ const indices = match.indices;
937
+ const groupIndices = indices?.groups;
938
+ let consumed = 0;
939
+ for (const { name, token } of pattern.groups) {
940
+ const raw = runValues.get(name) ?? match.groups[name];
941
+ const fromIndices = !runValues.has(name) && groupIndices?.[name];
942
+ const position = fromIndices ? fromIndices[0] : (match.index ?? 0) + consumed;
943
+ parts.push({ token, raw, position });
944
+ consumed += raw.length;
945
+ }
946
+ return parts;
947
+ }
948
+ function compileParser(formatStr, options = {}) {
949
+ if (formatStr.length > MAX_FORMAT_LENGTH) {
950
+ throw new FormatSyntaxError({
951
+ format: formatStr,
952
+ message: `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters (got ${formatStr.length}).`
953
+ });
954
+ }
955
+ const locale = options.locale ?? DEFAULT_LOCALE;
956
+ const pattern = getPattern(formatStr, locale);
957
+ return {
958
+ formatStr,
959
+ pattern,
960
+ parse(input, opts = {}) {
961
+ return parse(formatStr, input, { locale, ...opts });
962
+ },
963
+ safeParse(input, opts = {}) {
964
+ return safeParse(formatStr, input, { locale, ...opts });
965
+ },
966
+ tryParse(input, opts = {}) {
967
+ return tryParse(formatStr, input, { locale, ...opts });
968
+ },
969
+ parseToParts(input, opts = {}) {
970
+ return parseToParts(formatStr, input, { locale, ...opts });
971
+ }
972
+ };
973
+ }
974
+
975
+ export {
976
+ FORMAT_ONLY_TOKENS,
977
+ UNPADDED_NUMERIC_TOKENS,
978
+ parse,
979
+ safeParse,
980
+ tryParse,
981
+ parseToParts,
982
+ compileParser
983
+ };
984
+ //# sourceMappingURL=chunk-5UTV5VP6.js.map