temporal-fmt 0.8.93 → 0.8.95
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 +56 -4
- package/dist/calendarUtils.cjs +23 -2
- package/dist/calendarUtils.cjs.map +1 -1
- package/dist/calendarUtils.d.cts +11 -1
- package/dist/calendarUtils.d.ts +11 -1
- package/dist/calendarUtils.js +1 -1
- package/dist/chunk-26H4NR3J.js +155 -0
- package/dist/chunk-26H4NR3J.js.map +1 -0
- package/dist/{chunk-CWHSGH2Z.js → chunk-3ZXF3ELP.js} +7 -6
- package/dist/chunk-3ZXF3ELP.js.map +1 -0
- package/dist/{chunk-QL6WPSKN.js → chunk-46IMIRAG.js} +4 -4
- package/dist/{chunk-DXDPV25F.js → chunk-6EB3FIZL.js} +3 -3
- package/dist/{chunk-PGWSA5QP.js → chunk-6GBHT4HH.js} +2 -2
- package/dist/{chunk-V35OBXHF.js → chunk-AC66SDAS.js} +2 -2
- package/dist/{chunk-A76C5AGZ.js → chunk-ASLNQ4DG.js} +24 -3
- package/dist/chunk-ASLNQ4DG.js.map +1 -0
- package/dist/{chunk-5GBOYMJS.js → chunk-H5ZLOWP5.js} +2 -2
- package/dist/{chunk-7ZKINLTL.js → chunk-N3OPHFXP.js} +9 -2
- package/dist/chunk-N3OPHFXP.js.map +1 -0
- package/dist/{chunk-GIQZ24RL.js → chunk-RHGGZMLN.js} +2 -2
- package/dist/{chunk-5QZT6T4P.js → chunk-Z34HC4X5.js} +2 -2
- package/dist/duration.js +2 -2
- package/dist/format.cjs +113 -4
- package/dist/format.cjs.map +1 -1
- package/dist/format.d.cts +6 -5
- package/dist/format.d.ts +6 -5
- package/dist/format.js +3 -2
- package/dist/formatDistance.d.cts +23 -0
- package/dist/formatDistance.d.ts +23 -0
- package/dist/index.cjs +645 -504
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +133 -89
- package/dist/index.js.map +1 -1
- package/dist/interval.cjs +111 -2
- package/dist/interval.cjs.map +1 -1
- package/dist/interval.js +6 -5
- package/dist/numbering.d.cts +3 -0
- package/dist/numbering.d.ts +3 -0
- package/dist/parse.cjs +59 -0
- package/dist/parse.cjs.map +1 -1
- package/dist/parse.d.cts +10 -10
- package/dist/parse.d.ts +10 -10
- package/dist/parse.js +2 -2
- package/dist/parseRelative.d.cts +18 -0
- package/dist/parseRelative.d.ts +18 -0
- package/dist/recurrence.cjs.map +1 -1
- package/dist/recurrence.js +4 -4
- package/dist/relativeTime.cjs.map +1 -1
- package/dist/relativeTime.js +3 -3
- package/dist/timezone.cjs.map +1 -1
- package/dist/timezone.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-7ZKINLTL.js.map +0 -1
- package/dist/chunk-A76C5AGZ.js.map +0 -1
- package/dist/chunk-CWHSGH2Z.js.map +0 -1
- package/dist/chunk-RY7Q64ZS.js +0 -73
- package/dist/chunk-RY7Q64ZS.js.map +0 -1
- /package/dist/{chunk-QL6WPSKN.js.map → chunk-46IMIRAG.js.map} +0 -0
- /package/dist/{chunk-DXDPV25F.js.map → chunk-6EB3FIZL.js.map} +0 -0
- /package/dist/{chunk-PGWSA5QP.js.map → chunk-6GBHT4HH.js.map} +0 -0
- /package/dist/{chunk-V35OBXHF.js.map → chunk-AC66SDAS.js.map} +0 -0
- /package/dist/{chunk-5GBOYMJS.js.map → chunk-H5ZLOWP5.js.map} +0 -0
- /package/dist/{chunk-GIQZ24RL.js.map → chunk-RHGGZMLN.js.map} +0 -0
- /package/dist/{chunk-5QZT6T4P.js.map → chunk-Z34HC4X5.js.map} +0 -0
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ parse('h:mm a', '3:45 午後', { locale: 'ja-JP' });
|
|
|
205
205
|
parse('yyyy-MM-dd', '5786-11-21', { locale: 'en-u-ca-hebrew' }); // -u-ca- extension parses into that calendar
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
-
**Numeric fields
|
|
208
|
+
**Numeric fields come out in Western (0–9) digits by default**, regardless of locale — passing `{ locale: 'ar-EG' }` alone doesn't switch `yyyy`/`MM`/`dd` to Arabic-Indic digits, only the named-vocabulary tokens above. That's on purpose: logs, APIs, and filenames reading this output back in generally want boring ASCII digits, so locale doesn't silently drag the numeric tokens along with it. If you specifically want localized digits on the numeric pieces, that's a separate, explicit opt-in — see [Numbering systems](#numbering-systems) below.
|
|
209
209
|
|
|
210
210
|
### Registering custom vocabulary
|
|
211
211
|
|
|
@@ -407,6 +407,17 @@ formatDistance(in30d, today) // "next month" (r
|
|
|
407
407
|
|
|
408
408
|
Accepts `PlainDate`, `PlainDateTime`, or `ZonedDateTime`. A `PlainDate` is treated as midnight when diffed against a `PlainDateTime`. Throws on `PlainTime` (no anchor date to diff against) and on partial-date shapes (e.g. `{ year: 2026 }` with no month/day).
|
|
409
409
|
|
|
410
|
+
`formatDistanceToNow(date, options?)` is `formatDistance(date, now)`, reading the system clock at call time so you don't have to build the reference value yourself:
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
import { formatDistanceToNow } from 'temporal-fmt';
|
|
414
|
+
|
|
415
|
+
formatDistanceToNow(threeHoursAgo) // "3 hours ago"
|
|
416
|
+
formatDistanceToNow(tomorrow) // "in 1 day" (numeric: 'auto' reads "tomorrow")
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
It reads full wall-clock time (hour through millisecond) off the system clock, not just the calendar date — `formatRelativeToNow` below only needs day resolution, but `formatDistance`'s unit selection is millisecond-resolution, so a date-only reference here would misclassify anything under 24 hours old. The reference is captured fresh on every call, never cached, so back-to-back calls reflect whatever "now" actually is at each call site.
|
|
420
|
+
|
|
410
421
|
### `formatRelative` / `formatRelativeToNow` — "yesterday", "last week"
|
|
411
422
|
|
|
412
423
|
`formatRelative(date1, date2, options?)` returns a calendar-relative label ("yesterday", "tomorrow", "last week") rather than `formatDistance`'s numeric-distance phrasing. `formatRelativeToNow(date, options?)` is `formatRelative(date, now)`.
|
|
@@ -469,6 +480,22 @@ The "next X on X = 7 days out, not today" convention holds across all four suppo
|
|
|
469
480
|
|
|
470
481
|
`parseRelative` throws a descriptive error for any phrase it doesn't recognize, naming the supported categories. Accepts `PlainDate`, `PlainDateTime`, or `ZonedDateTime` as the reference (needs `dayOfWeek` to compute weekday offsets); throws on `PlainTime`.
|
|
471
482
|
|
|
483
|
+
### Typo tolerance (`{ fuzzy: true }`)
|
|
484
|
+
|
|
485
|
+
Strict matching is the default for the same reason `parse()` throws on ambiguous input rather than guessing — a wrong guess that looks plausible is worse than a hard stop. But if you're taking free-text input from a person instead of a controlled phrase set, typos are routine, not exceptional. `{ fuzzy: true }` opts into correcting them:
|
|
486
|
+
|
|
487
|
+
```js
|
|
488
|
+
parseRelative('tommorow', today, { fuzzy: true }).toString() // '2026-08-05'
|
|
489
|
+
parseRelative('next tuesady', today, { fuzzy: true }).toString() // '2026-08-11'
|
|
490
|
+
parseRelative('tommorow', today); // throws — fuzzy is opt-in
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
It works by tokenizing the input on whitespace and, for any word that isn't already an exact match against the English vocabulary (weekday names, month names, and marker words like "next"/"ago"/"days"), finding the closest vocabulary word within edit distance 2 and substituting it before re-running the exact matcher. Distance 2 covers the common cases — a dropped letter ("tommorow") is distance 1, a transposed pair ("tuesady") is distance 2 under plain Levenshtein — without opening the door to correcting a word into something only vaguely similar.
|
|
494
|
+
|
|
495
|
+
Numbers are never touched by fuzzy correction, on purpose: `"5 dyas"` fuzzy-corrects to `"5 days"` and then still throws the usual past-or-future ambiguity error, since "in"/"ago" disambiguation is a separate, stricter contract this option doesn't relax. Digit typos are also a much easier way to silently produce a wrong date than a weekday-name typo is, so they stay out of scope here.
|
|
496
|
+
|
|
497
|
+
Fuzzy mode is English-only for now. The other three grammars have enough positional and multi-word-marker variation — French's "il y a", Spanish's pre/post weekday-modifier forms — that one word-substitution corrector doesn't fit all of them without per-language tuning this doesn't attempt yet. Combining `{ fuzzy: true }` with a non-English `locale` throws a scope-limited error rather than silently skipping the correction pass, so a caller relying on it for, say, French input gets a clear signal instead of a confusing "doesn't recognize" error with no indication fuzzy matching never ran.
|
|
498
|
+
|
|
472
499
|
**Adding a language**: `registerRelativeGrammar(grammar)` registers a new language's phrase patterns without touching the built-in four. `listRegisteredGrammars()` lists what's currently registered.
|
|
473
500
|
|
|
474
501
|
## Date arithmetic, comparison, and rounding
|
|
@@ -523,9 +550,21 @@ getQuarter(date);
|
|
|
523
550
|
- `daysInMonth(value)`, `daysInYear(value)`, `monthsInYear(value)`.
|
|
524
551
|
- `isLeapYear(value)`, `isLeapMonth(value)` (Gregorian: `isLeapMonth` always returns `false`).
|
|
525
552
|
- `dayOfYear(value)`, `weekOfYear(value)`, `weekYear(value)`.
|
|
526
|
-
- `getQuarter(value)`, `getMonth(value)`, `getWeekday(value)`.
|
|
553
|
+
- `getQuarter(value, options?)`, `getMonth(value)`, `getWeekday(value)`.
|
|
527
554
|
- `startOf(value, unit)` / `endOf(value, unit)` — `unit` is `'day' | 'month' | 'year' | 'hour' | 'minute' | 'second'`. Returns a field bag with finer fields zeroed (`startOf`) or extended to their max (`endOf`).
|
|
528
555
|
|
|
556
|
+
`getQuarter` defaults to calendar quarters (Jan–Mar = Q1, same as the `Q`/`QQQ` format tokens), but a fiscal year rarely starts in January. Pass `{ startMonth }` to shift which month counts as fiscal month 1:
|
|
557
|
+
|
|
558
|
+
```js
|
|
559
|
+
import { getQuarter } from 'temporal-fmt';
|
|
560
|
+
|
|
561
|
+
getQuarter({ month: 8 }); // 3 — calendar quarter (default)
|
|
562
|
+
getQuarter({ month: 8 }, { startMonth: 7 }); // 1 — fiscal year starting July (UK/India-style)
|
|
563
|
+
getQuarter({ month: 8 }, { startMonth: 10 }); // 4 — fiscal year starting October (Apple's FY)
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
`startMonth` must be an integer 1–12; anything else throws rather than silently defaulting. This is a separate function from the `Q`/`QQQ` tokens, not a shared implementation — those tokens compute quarter inline from month and have no fiscal-offset option, so a fiscal quarter number isn't currently something `format()`/`parse()` can render or round-trip through a token string. Use `getQuarter` directly for fiscal reporting and reach for the tokens only when you actually want calendar quarters in formatted output.
|
|
567
|
+
|
|
529
568
|
**Gregorian-only, documented limitation**: `daysInMonth`, `daysInYear`, `isLeapYear`, `monthsInYear` (always 12), `isLeapMonth` (always `false`), `dayOfYear`, `weekOfYear`, and `weekYear` all use Gregorian rules and will give wrong answers on non-Gregorian calendars (Hebrew, Islamic, etc.). For those, use the `Temporal` value's own calendar-aware properties directly instead:
|
|
530
569
|
|
|
531
570
|
```js
|
|
@@ -751,6 +790,19 @@ assertZonedDateTime(value); // throws descriptively if it isn't one
|
|
|
751
790
|
|
|
752
791
|
## Numbering systems
|
|
753
792
|
|
|
793
|
+
The [Locales](#locales) section above notes that numeric tokens always render Western digits by default — that's still the default, but it's opt-out rather than fixed, via `{ numberingSystem }` on `format()` and `{ parseNumberingSystem }` on `parse()`:
|
|
794
|
+
|
|
795
|
+
```js
|
|
796
|
+
import { format, parse } from 'temporal-fmt';
|
|
797
|
+
|
|
798
|
+
format(date, 'yyyy-MM-dd', { numberingSystem: 'arab' }); // "٢٠٢٦-٠٨-٠٤"
|
|
799
|
+
parse('yyyy-MM-dd', '٢٠٢٦-٠٨-٠٤', { parseNumberingSystem: 'arab' }).toString(); // '2026-08-04'
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
The two option names are deliberately different (`numberingSystem` vs. `parseNumberingSystem`), not a naming inconsistency — the two directions aren't always symmetric. You might want Arabic-Indic digits in your UI output without expecting Arabic-Indic digits back on input, or the reverse, so a caller mixing `format()` and `parse()` options in one config object can set each independently. `formatToParts()` applies numbering per-part rather than once at the end, so a caller styling individual token parts (one `<span>` per token, say) still gets correctly-transliterated digits in each part instead of plain ASCII. An unsupported system name throws immediately rather than silently falling back to `'latn'`.
|
|
803
|
+
|
|
804
|
+
If you'd rather convert digits yourself instead of going through `format()`/`parse()`'s options — say, transliterating a string that came from somewhere else entirely — the underlying conversion is available directly:
|
|
805
|
+
|
|
754
806
|
```js
|
|
755
807
|
import { convertDigits, convertDigitsToAscii } from 'temporal-fmt';
|
|
756
808
|
|
|
@@ -760,7 +812,7 @@ convertDigitsToAscii('٢٠٢٦', 'arab'); // "2026"
|
|
|
760
812
|
|
|
761
813
|
- `convertDigits(s, system)` — ASCII digits to a locale's native digits.
|
|
762
814
|
- `convertDigitsToAscii(s, system)` — the inverse.
|
|
763
|
-
- `applyNumbering(s, options)` / `applyParseNumbering(s, options)` — the
|
|
815
|
+
- `applyNumbering(s, options)` / `applyParseNumbering(s, options)` — the same helpers `format()`/`parse()` call internally for `{ numberingSystem }` / `{ parseNumberingSystem }`, exposed directly for anyone building their own formatting layer on top rather than going through `format()`/`parse()`.
|
|
764
816
|
- `SUPPORTED_NUMBERING_SYSTEMS` — the set of supported system names (`'latn' | 'arab' | 'deva' | 'beng' | 'guru' | 'gujr' | 'orya' | 'tamldec' | 'telu' | 'knda' | 'mlym' | 'fullwide' | 'hanidec'`).
|
|
765
817
|
|
|
766
818
|
## Extending with custom tokens
|
|
@@ -911,7 +963,7 @@ Migrate file by file, dropping the wrapper once nothing calls the old path anymo
|
|
|
911
963
|
|
|
912
964
|
## Known limitations
|
|
913
965
|
|
|
914
|
-
- **Numerals
|
|
966
|
+
- **Numerals default to Western digits** in numeric tokens, regardless of locale, unless you opt into `{ numberingSystem }` / `{ parseNumberingSystem }` — see [Numbering systems](#numbering-systems).
|
|
915
967
|
- **Locale-aware tokens need Node 20+**, native or polyfilled. Untested below that.
|
|
916
968
|
- **You must provide a Temporal implementation** on anything below Node 26 — see [Providing `Temporal`](#providing-temporal).
|
|
917
969
|
- **Pre-1582 dates and locale-aware tokens don't mix well on native Temporal (Node 26+).** `MMMM`/`MMM`/`EEEE`/`EEE` can render the wrong month or weekday for dates before roughly 1582 CE. This is an ICU limitation, not a bug here: ICU's default Gregorian calendar cutover is October 15, 1582, so `Intl.DateTimeFormat.formatToParts()` silently reinterprets earlier dates under the Julian calendar even though `Temporal` itself uses a proleptic Gregorian calendar throughout — see [tc39/ecma402#1003](https://github.com/tc39/ecma402/issues/1003). Numeric tokens never touch `Intl` and aren't affected.
|
package/dist/calendarUtils.cjs
CHANGED
|
@@ -125,9 +125,22 @@ function weekYear(view) {
|
|
|
125
125
|
requireFields(view, ["year", "month", "day", "dayOfWeek"]);
|
|
126
126
|
return isoWeekYearAndWeek(view.year, view.month, view.day, view.dayOfWeek).isoYear;
|
|
127
127
|
}
|
|
128
|
-
function
|
|
128
|
+
function validateStartMonth(startMonth) {
|
|
129
|
+
if (!Number.isInteger(startMonth) || startMonth < 1 || startMonth > 12) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`temporal-fmt: getQuarter's startMonth must be an integer from 1 to 12 (got ${startMonth}).`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function getQuarter(view, options = {}) {
|
|
129
136
|
requireFields(view, ["month"]);
|
|
130
|
-
|
|
137
|
+
const startMonth = options.startMonth ?? 1;
|
|
138
|
+
validateStartMonth(startMonth);
|
|
139
|
+
if (startMonth === 1) {
|
|
140
|
+
return Math.ceil(view.month / 3);
|
|
141
|
+
}
|
|
142
|
+
const shifted = (view.month - startMonth + 12) % 12 + 1;
|
|
143
|
+
return Math.ceil(shifted / 3);
|
|
131
144
|
}
|
|
132
145
|
function getMonth(view) {
|
|
133
146
|
requireFields(view, ["month"]);
|
|
@@ -140,6 +153,12 @@ function getWeekday(view) {
|
|
|
140
153
|
function touchesTime(unit) {
|
|
141
154
|
return unit === "day" || unit === "month" || unit === "year";
|
|
142
155
|
}
|
|
156
|
+
function recomputeDayOfWeek(view) {
|
|
157
|
+
if (typeof view.dayOfWeek !== "number") return;
|
|
158
|
+
if (typeof view.year !== "number" || typeof view.month !== "number" || typeof view.day !== "number") return;
|
|
159
|
+
const jsDow = new Date(Date.UTC(view.year, view.month - 1, view.day)).getUTCDay();
|
|
160
|
+
view.dayOfWeek = jsDow === 0 ? 7 : jsDow;
|
|
161
|
+
}
|
|
143
162
|
function startOf(value, unit) {
|
|
144
163
|
const view = asDateFieldView(value);
|
|
145
164
|
const result = { ...view };
|
|
@@ -149,6 +168,7 @@ function startOf(value, unit) {
|
|
|
149
168
|
} else if (unit === "month") {
|
|
150
169
|
result.day = 1;
|
|
151
170
|
}
|
|
171
|
+
recomputeDayOfWeek(result);
|
|
152
172
|
if (touchesTime(unit)) {
|
|
153
173
|
result.hour = 0;
|
|
154
174
|
result.minute = 0;
|
|
@@ -175,6 +195,7 @@ function endOf(value, unit) {
|
|
|
175
195
|
} else if (unit === "month") {
|
|
176
196
|
result.day = daysInMonth({ year: result.year, month: result.month });
|
|
177
197
|
}
|
|
198
|
+
recomputeDayOfWeek(result);
|
|
178
199
|
if (touchesTime(unit)) {
|
|
179
200
|
result.hour = 23;
|
|
180
201
|
result.minute = 59;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/calendarUtils.ts","../src/isoWeek.ts"],"sourcesContent":["// Calendar utility helpers (plan section L). These are pure functions\n// over the TemporalLike shape — no Temporal namespace needed, same\n// approach as isoWeek.ts and the field-reading helpers in format.ts.\n// Letting callers compute dayOfYear/weekOfYear/etc. without going\n// through format() means they can build their own derived values\n// without committing to a string format.\n//\n// Calendar-sensitivity: the helpers in this module assume the\n// iso8601 (Gregorian) calendar — that's what TemporalLike fields\n// carry for the overwhelming majority of callers. Non-Gregorian\n// calendars (hebrew, islamic, etc.) need their own helpers; this\n// module doesn't try to be calendar-polymorphic the way Temporal\n// itself is. Documented limitation, not a design choice — see\n// VERIFICATION.md for the rationale.\n\nimport { isGregorianLeapYear, dayOfYear, isoWeekYearAndWeek } from './isoWeek.js';\n\n// A subset of TemporalLike that has the date fields these helpers need,\n// plus optional time fields. PlainTime isn't a DateFieldView (no\n// year/month/day), but PlainDateTime / ZonedDateTime / PlainDate all\n// match. Time fields are optional so callers can pass a PlainDate\n// to startOf(value, 'month') without having to populate hour/minute/etc.\n// Exported so the comparison/arithmetic modules can use the same\n// narrowing.\nexport interface DateFieldView {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n dayOfWeek?: number;\n calendarId?: string;\n}\n\nfunction requireFields(view: DateFieldView, fields: Array<keyof DateFieldView>): void {\n for (const f of fields) {\n if (typeof view[f] !== 'number') {\n throw new Error(\n `temporal-fmt: calendar helper requires \"${String(f)}\", which this value doesn't have. ` +\n `Pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`\n );\n }\n }\n}\n\nexport function daysInMonth(view: DateFieldView): number {\n requireFields(view, ['year', 'month']);\n const { year, month } = view;\n // Standard Gregorian month lengths. February's length depends on\n // whether `year` is a leap year — the same isGregorianLeapYear check\n // isoWeek.ts uses for dayOfYear arithmetic.\n const LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n if (month === 2 && isGregorianLeapYear(year!)) return 29;\n return LENGTHS[(month! - 1)!]!;\n}\n\nexport function daysInYear(view: DateFieldView): 365 | 366 {\n requireFields(view, ['year']);\n return isGregorianLeapYear(view.year!) ? 366 : 365;\n}\n\nexport function monthsInYear(_view: DateFieldView): 12 {\n // Gregorian always has 12 months. Other calendars (Hebrew leap years\n // have 13) need calendar-aware logic this module doesn't carry — see\n // the file-level comment. The `_view` parameter is kept so the\n // signature mirrors the other helpers and a future calendar-aware\n // implementation can use it without changing call sites.\n return 12;\n}\n\nexport function isLeapYear(view: DateFieldView): boolean {\n requireFields(view, ['year']);\n return isGregorianLeapYear(view.year!);\n}\n\n// `isLeapMonth` would require knowing which month of a leap-year-aware\n// calendar is the leap month — Gregorian doesn't have one, so this\n// returns false unconditionally. Kept here so the public surface\n// matches the plan's section L listing; non-Gregorian calendars need\n// a different implementation.\nexport function isLeapMonth(_view: DateFieldView): boolean {\n return false;\n}\n\nexport function dayOfYearHelper(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day']);\n return dayOfYear(view.year!, view.month!, view.day!);\n}\n\n// ISO 8601 week and week-year. Delegates to isoWeek.ts's\n// isoWeekYearAndWeek, which does the full Thursday-of-week\n// computation to handle the year-boundary cases (Dec 29-31 belonging\n// to week 1 of next year, Jan 1-3 belonging to week 52/53 of the\n// previous year).\nexport function weekOfYear(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day', 'dayOfWeek']);\n return isoWeekYearAndWeek(view.year!, view.month!, view.day!, view.dayOfWeek!).week;\n}\n\nexport function weekYear(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day', 'dayOfWeek']);\n return isoWeekYearAndWeek(view.year!, view.month!, view.day!, view.dayOfWeek!).isoYear;\n}\n\nexport function getQuarter(view: DateFieldView): number {\n requireFields(view, ['month']);\n // Mirrors the Q token: months 1-3 → Q1, 4-6 → Q2, 7-9 → Q3, 10-12 → Q4.\n return Math.ceil(view.month! / 3);\n}\n\n// `getMonth` / `getWeekday` look trivial (just read the field) but the\n// plan's section L lists them explicitly, so they're here for surface\n// completeness. They also normalize: getWeekday returns 1-7 (Mon-Sun,\n// matching Temporal's spec) regardless of what numbering the caller's\n// underlying value uses.\nexport function getMonth(view: DateFieldView): number {\n requireFields(view, ['month']);\n return view.month!;\n}\n\nexport function getWeekday(view: DateFieldView): number {\n requireFields(view, ['dayOfWeek']);\n return view.dayOfWeek!;\n}\n\n// startOf / endOf return new field bags (not Temporal objects — this\n// module is polyfill-free) with the relevant fields zeroed/extended.\n// Callers can pass the result to a Temporal constructor if they want\n// a typed value.\nexport type StartOfUnit = 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second';\n\n// Returns true if the given unit (when used with startOf/endOf) should\n// also touch the time fields. 'day', 'month', 'year' all imply a\n// resolution coarser than an hour, so startOf zeroes the time fields\n// and endOf maxes them. Sub-hour units (hour/minute/second) only touch\n// the fields finer than themselves.\nfunction touchesTime(unit: StartOfUnit): boolean {\n return unit === 'day' || unit === 'month' || unit === 'year';\n}\n\nexport function startOf(value: unknown, unit: StartOfUnit): DateFieldView {\n const view = asDateFieldView(value);\n const result: DateFieldView = { ...view };\n if (unit === 'year') {\n result.month = 1;\n result.day = 1;\n } else if (unit === 'month') {\n result.day = 1;\n }\n if (touchesTime(unit)) {\n result.hour = 0;\n result.minute = 0;\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'hour') {\n result.minute = 0;\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'minute') {\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'second') {\n result.millisecond = 0;\n }\n return result;\n}\n\nexport function endOf(value: unknown, unit: StartOfUnit): DateFieldView {\n const view = asDateFieldView(value);\n const result: DateFieldView = { ...view };\n if (unit === 'year') {\n result.month = 12;\n result.day = daysInMonth({ year: result.year!, month: 12 });\n } else if (unit === 'month') {\n result.day = daysInMonth({ year: result.year!, month: result.month! });\n }\n if (touchesTime(unit)) {\n result.hour = 23;\n result.minute = 59;\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'hour') {\n result.minute = 59;\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'minute') {\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'second') {\n result.millisecond = 999;\n }\n return result;\n}\n\n// Type-narrowing helpers used by the comparison/arithmetic modules.\n// Lets them accept any of the four date-carrying Temporal types without\n// importing Temporal itself.\nexport function asDateFieldView(value: unknown): DateFieldView {\n if (typeof value !== 'object' || value === null) {\n throw new Error(`temporal-fmt: expected a date-carrying Temporal value, got ${String(value)}.`);\n }\n // Temporal instances expose year/month/day/etc. as prototype getters,\n // not own enumerable properties — so `{ ...value }` would lose them.\n // Read them explicitly. Only the fields actually present on this\n // value type end up in the returned view.\n const v = value as Record<string, unknown>;\n const out: DateFieldView = {};\n if (typeof v.year === 'number') out.year = v.year;\n if (typeof v.month === 'number') out.month = v.month;\n if (typeof v.day === 'number') out.day = v.day;\n if (typeof v.hour === 'number') out.hour = v.hour;\n if (typeof v.minute === 'number') out.minute = v.minute;\n if (typeof v.second === 'number') out.second = v.second;\n if (typeof v.millisecond === 'number') out.millisecond = v.millisecond;\n if (typeof v.dayOfWeek === 'number') out.dayOfWeek = v.dayOfWeek;\n if (typeof v.calendarId === 'string') out.calendarId = v.calendarId;\n if (out.year === undefined || out.month === undefined || out.day === undefined) {\n throw new Error(\n `temporal-fmt: value is missing year/month/day fields — pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`\n );\n }\n return out;\n}\n\n// Re-export the TemporalType alias so callers can import everything\n// from one place.\nexport type { TemporalType } from './tokenMetadata.js';\n// Re-export TemporalLike for the same reason.\nexport type { TemporalLike } from './tokens.js';\n","// ISO 8601 week numbering: a week runs Monday–Sunday, and week 1 of a year\n// is the week containing the year's first Thursday (equivalently, the week\n// containing January 4). This means late-December dates can fall in week 1\n// of the *next* year, and early-January dates can fall in week 52 or 53 of\n// the *previous* year. The \"ISO week-numbering year\" (what `RRRR` formats)\n// is that adjacent year, not the calendar year.\n//\n// Computed here from the date's own year/month/day plus its ISO dayOfWeek\n// (1=Mon..7=Sun, matching Temporal's numbering) using plain Gregorian\n// arithmetic — no Temporal factory needed. format() only has the fields\n// the caller already put on the object, and requiring a Temporal\n// implementation just for ISO week would be a regression for callers who\n// use format() without setTemporal() on a non-26 Node.\n\nconst DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\nconst CUMULATIVE_DAYS_BY_MONTH = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];\n\nexport function isGregorianLeapYear(year: number): boolean {\n // Gregorian rule: divisible by 4, except centuries which must also be\n // divisible by 400. Temporal's iso8601 calendar is proleptic Gregorian\n // (no Julian cutover), so this applies for every year, including BCE.\n return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n}\n\nfunction daysInYear(year: number): 365 | 366 {\n return isGregorianLeapYear(year) ? 366 : 365;\n}\n\nexport function dayOfYear(year: number, month: number, day: number): number {\n let doy = CUMULATIVE_DAYS_BY_MONTH[month - 1]! + day;\n if (month > 2 && isGregorianLeapYear(year)) doy += 1;\n return doy;\n}\n\n// Jan 1, 2000 was a Saturday — ISO dayOfWeek 6. Anchoring day-of-week\n// computations to a known reference date is simpler and cheaper than\n// pulling in Zeller's congruence, and the reference never changes.\nconst REFERENCE_YEAR = 2000;\nconst REFERENCE_JAN1_DAY_OF_WEEK = 6;\n\nfunction dayOfWeekOfJan1(year: number): number {\n // Sum full-year deltas from the 2000 anchor rather than recomputing from\n // scratch each call — the per-call work is a single mod this way, and\n // the loop rarely runs far (a typical caller passes a current-era year).\n let offset = 0;\n if (year >= REFERENCE_YEAR) {\n for (let y = REFERENCE_YEAR; y < year; y++) offset += daysInYear(y);\n } else {\n for (let y = year; y < REFERENCE_YEAR; y++) offset -= daysInYear(y);\n }\n // Convert \"days since Jan 1, 2000\" into an ISO day-of-week (1=Mon..7=Sun).\n // Jan 1, 2000 was ISO 6 (Sat), so zero-indexed dow = (6-1 + offset) mod 7.\n const zeroIndexed = (((6 - 1 + offset) % 7) + 7) % 7;\n return zeroIndexed + 1;\n}\n\nexport interface IsoWeekDate {\n isoYear: number;\n week: number; // 1..53\n}\n\nexport function isoWeekYearAndWeek(year: number, month: number, day: number, dayOfWeek: number): IsoWeekDate {\n // Step 1: find the Thursday of the current ISO week. The ISO week-numbering\n // year is whichever calendar year that Thursday falls in. Computing it via\n // day-of-year offsets (rather than constructing a Temporal.PlainDate and\n // adding days) keeps this function pure-numeric.\n const doy = dayOfYear(year, month, day);\n const thursdayDoyRelative = doy + (4 - dayOfWeek); // may be <1 or >daysInYear\n\n let isoYear: number;\n let thursdayDoy: number;\n if (thursdayDoyRelative < 1) {\n isoYear = year - 1;\n thursdayDoy = thursdayDoyRelative + daysInYear(isoYear);\n } else if (thursdayDoyRelative > daysInYear(year)) {\n isoYear = year + 1;\n thursdayDoy = thursdayDoyRelative - daysInYear(year);\n } else {\n isoYear = year;\n thursdayDoy = thursdayDoyRelative;\n }\n\n // Step 2: locate the first Thursday of isoYear — its week is week 1. The\n // first Thursday's day-of-year depends on what weekday Jan 1 of isoYear is.\n const jan1Dow = dayOfWeekOfJan1(isoYear);\n const firstThursdayDoy = 1 + ((4 - jan1Dow + 7) % 7); // 1..7\n\n // Step 3: count full weeks between the two Thursdays.\n const week = 1 + Math.floor((thursdayDoy - firstThursdayDoy) / 7);\n return { isoYear, week };\n}\n"],"mappings":"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","names":["daysInYear","daysInYear"]}
|
|
1
|
+
{"version":3,"sources":["../src/calendarUtils.ts","../src/isoWeek.ts"],"sourcesContent":["// Calendar utility helpers (plan section L). These are pure functions\n// over the TemporalLike shape — no Temporal namespace needed, same\n// approach as isoWeek.ts and the field-reading helpers in format.ts.\n// Letting callers compute dayOfYear/weekOfYear/etc. without going\n// through format() means they can build their own derived values\n// without committing to a string format.\n//\n// Calendar-sensitivity: the helpers in this module assume the\n// iso8601 (Gregorian) calendar — that's what TemporalLike fields\n// carry for the overwhelming majority of callers. Non-Gregorian\n// calendars (hebrew, islamic, etc.) need their own helpers; this\n// module doesn't try to be calendar-polymorphic the way Temporal\n// itself is. Documented limitation, not a design choice — see\n// VERIFICATION.md for the rationale.\n\nimport { isGregorianLeapYear, dayOfYear, isoWeekYearAndWeek } from './isoWeek.js';\n\n// A subset of TemporalLike that has the date fields these helpers need,\n// plus optional time fields. PlainTime isn't a DateFieldView (no\n// year/month/day), but PlainDateTime / ZonedDateTime / PlainDate all\n// match. Time fields are optional so callers can pass a PlainDate\n// to startOf(value, 'month') without having to populate hour/minute/etc.\n// Exported so the comparison/arithmetic modules can use the same\n// narrowing.\nexport interface DateFieldView {\n year?: number;\n month?: number;\n day?: number;\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n dayOfWeek?: number;\n calendarId?: string;\n}\n\nfunction requireFields(view: DateFieldView, fields: Array<keyof DateFieldView>): void {\n for (const f of fields) {\n if (typeof view[f] !== 'number') {\n throw new Error(\n `temporal-fmt: calendar helper requires \"${String(f)}\", which this value doesn't have. ` +\n `Pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`\n );\n }\n }\n}\n\nexport function daysInMonth(view: DateFieldView): number {\n requireFields(view, ['year', 'month']);\n const { year, month } = view;\n // Standard Gregorian month lengths. February's length depends on\n // whether `year` is a leap year — the same isGregorianLeapYear check\n // isoWeek.ts uses for dayOfYear arithmetic.\n const LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n if (month === 2 && isGregorianLeapYear(year!)) return 29;\n return LENGTHS[(month! - 1)!]!;\n}\n\nexport function daysInYear(view: DateFieldView): 365 | 366 {\n requireFields(view, ['year']);\n return isGregorianLeapYear(view.year!) ? 366 : 365;\n}\n\nexport function monthsInYear(_view: DateFieldView): 12 {\n // Gregorian always has 12 months. Other calendars (Hebrew leap years\n // have 13) need calendar-aware logic this module doesn't carry — see\n // the file-level comment. The `_view` parameter is kept so the\n // signature mirrors the other helpers and a future calendar-aware\n // implementation can use it without changing call sites.\n return 12;\n}\n\nexport function isLeapYear(view: DateFieldView): boolean {\n requireFields(view, ['year']);\n return isGregorianLeapYear(view.year!);\n}\n\n// `isLeapMonth` would require knowing which month of a leap-year-aware\n// calendar is the leap month — Gregorian doesn't have one, so this\n// returns false unconditionally. Kept here so the public surface\n// matches the plan's section L listing; non-Gregorian calendars need\n// a different implementation.\nexport function isLeapMonth(_view: DateFieldView): boolean {\n return false;\n}\n\nexport function dayOfYearHelper(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day']);\n return dayOfYear(view.year!, view.month!, view.day!);\n}\n\n// ISO 8601 week and week-year. Delegates to isoWeek.ts's\n// isoWeekYearAndWeek, which does the full Thursday-of-week\n// computation to handle the year-boundary cases (Dec 29-31 belonging\n// to week 1 of next year, Jan 1-3 belonging to week 52/53 of the\n// previous year).\nexport function weekOfYear(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day', 'dayOfWeek']);\n return isoWeekYearAndWeek(view.year!, view.month!, view.day!, view.dayOfWeek!).week;\n}\n\nexport function weekYear(view: DateFieldView): number {\n requireFields(view, ['year', 'month', 'day', 'dayOfWeek']);\n return isoWeekYearAndWeek(view.year!, view.month!, view.day!, view.dayOfWeek!).isoYear;\n}\n\n/**\n * Fiscal-quarter options. `startMonth` is the calendar month (1-12) the\n * fiscal year begins on — e.g. `7` for a fiscal year starting in July.\n * Omitted or `1` gives the calendar-quarter behavior getQuarter() has\n * always had (Jan-Mar = Q1, etc.), so existing callers passing nothing\n * see no change.\n */\nexport interface QuarterOptions {\n startMonth?: number;\n}\n\nfunction validateStartMonth(startMonth: number): void {\n if (!Number.isInteger(startMonth) || startMonth < 1 || startMonth > 12) {\n throw new Error(\n `temporal-fmt: getQuarter's startMonth must be an integer from 1 to 12 (got ${startMonth}).`\n );\n }\n}\n\nexport function getQuarter(view: DateFieldView, options: QuarterOptions = {}): number {\n requireFields(view, ['month']);\n const startMonth = options.startMonth ?? 1;\n validateStartMonth(startMonth);\n if (startMonth === 1) {\n // Mirrors the Q token: months 1-3 → Q1, 4-6 → Q2, 7-9 → Q3, 10-12 → Q4.\n return Math.ceil(view.month! / 3);\n }\n // Fiscal case: shift the month so startMonth becomes month 1 of the\n // fiscal year (mod 12, 1-indexed), then apply the same ceil(/3) rule.\n // E.g. startMonth=7 (fiscal year starts July): July→1, Aug→2, ...,\n // Dec→6, Jan→7, ..., June→12. Then Q1 = fiscal months 1-3 (Jul-Sep),\n // matching the common \"FY starts in July\" convention where Q1 is the\n // first quarter of the fiscal year, not a quarter numbered by which\n // calendar quarter it falls in.\n const shifted = ((view.month! - startMonth + 12) % 12) + 1;\n return Math.ceil(shifted / 3);\n}\n\n// `getMonth` / `getWeekday` look trivial (just read the field) but the\n// plan's section L lists them explicitly, so they're here for surface\n// completeness. They also normalize: getWeekday returns 1-7 (Mon-Sun,\n// matching Temporal's spec) regardless of what numbering the caller's\n// underlying value uses.\nexport function getMonth(view: DateFieldView): number {\n requireFields(view, ['month']);\n return view.month!;\n}\n\nexport function getWeekday(view: DateFieldView): number {\n requireFields(view, ['dayOfWeek']);\n return view.dayOfWeek!;\n}\n\n// startOf / endOf return new field bags (not Temporal objects — this\n// module is polyfill-free) with the relevant fields zeroed/extended.\n// Callers can pass the result to a Temporal constructor if they want\n// a typed value.\nexport type StartOfUnit = 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second';\n\n// Returns true if the given unit (when used with startOf/endOf) should\n// also touch the time fields. 'day', 'month', 'year' all imply a\n// resolution coarser than an hour, so startOf zeroes the time fields\n// and endOf maxes them. Sub-hour units (hour/minute/second) only touch\n// the fields finer than themselves.\nfunction touchesTime(unit: StartOfUnit): boolean {\n return unit === 'day' || unit === 'month' || unit === 'year';\n}\n\n// startOf/endOf reassign year/month/day, which invalidates any\n// dayOfWeek carried over from the input — a plain { ...view } spread\n// leaves the old value sitting there unchanged. Same failure mode\n// businessCalendar.ts's isBusinessDay() works around for add(); we\n// recompute here rather than trust the copied field.\nfunction recomputeDayOfWeek(view: DateFieldView): void {\n if (typeof view.dayOfWeek !== 'number') return;\n if (typeof view.year !== 'number' || typeof view.month !== 'number' || typeof view.day !== 'number') return;\n const jsDow = new Date(Date.UTC(view.year, view.month - 1, view.day)).getUTCDay(); // 0=Sun..6=Sat\n view.dayOfWeek = jsDow === 0 ? 7 : jsDow; // 1=Mon..7=Sun\n}\n\nexport function startOf(value: unknown, unit: StartOfUnit): DateFieldView {\n const view = asDateFieldView(value);\n const result: DateFieldView = { ...view };\n if (unit === 'year') {\n result.month = 1;\n result.day = 1;\n } else if (unit === 'month') {\n result.day = 1;\n }\n recomputeDayOfWeek(result);\n if (touchesTime(unit)) {\n result.hour = 0;\n result.minute = 0;\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'hour') {\n result.minute = 0;\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'minute') {\n result.second = 0;\n result.millisecond = 0;\n } else if (unit === 'second') {\n result.millisecond = 0;\n }\n return result;\n}\n\nexport function endOf(value: unknown, unit: StartOfUnit): DateFieldView {\n const view = asDateFieldView(value);\n const result: DateFieldView = { ...view };\n if (unit === 'year') {\n result.month = 12;\n result.day = daysInMonth({ year: result.year!, month: 12 });\n } else if (unit === 'month') {\n result.day = daysInMonth({ year: result.year!, month: result.month! });\n }\n recomputeDayOfWeek(result);\n if (touchesTime(unit)) {\n result.hour = 23;\n result.minute = 59;\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'hour') {\n result.minute = 59;\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'minute') {\n result.second = 59;\n result.millisecond = 999;\n } else if (unit === 'second') {\n result.millisecond = 999;\n }\n return result;\n}\n\n// Type-narrowing helpers used by the comparison/arithmetic modules.\n// Lets them accept any of the four date-carrying Temporal types without\n// importing Temporal itself.\nexport function asDateFieldView(value: unknown): DateFieldView {\n if (typeof value !== 'object' || value === null) {\n throw new Error(`temporal-fmt: expected a date-carrying Temporal value, got ${String(value)}.`);\n }\n // Temporal instances expose year/month/day/etc. as prototype getters,\n // not own enumerable properties — so `{ ...value }` would lose them.\n // Read them explicitly. Only the fields actually present on this\n // value type end up in the returned view.\n const v = value as Record<string, unknown>;\n const out: DateFieldView = {};\n if (typeof v.year === 'number') out.year = v.year;\n if (typeof v.month === 'number') out.month = v.month;\n if (typeof v.day === 'number') out.day = v.day;\n if (typeof v.hour === 'number') out.hour = v.hour;\n if (typeof v.minute === 'number') out.minute = v.minute;\n if (typeof v.second === 'number') out.second = v.second;\n if (typeof v.millisecond === 'number') out.millisecond = v.millisecond;\n if (typeof v.dayOfWeek === 'number') out.dayOfWeek = v.dayOfWeek;\n if (typeof v.calendarId === 'string') out.calendarId = v.calendarId;\n if (out.year === undefined || out.month === undefined || out.day === undefined) {\n throw new Error(\n `temporal-fmt: value is missing year/month/day fields — pass a Temporal.PlainDate / PlainDateTime / ZonedDateTime.`\n );\n }\n return out;\n}\n\n// Re-export the TemporalType alias so callers can import everything\n// from one place.\nexport type { TemporalType } from './tokenMetadata.js';\n// Re-export TemporalLike for the same reason.\nexport type { TemporalLike } from './tokens.js';\n","// ISO 8601 week numbering: a week runs Monday–Sunday, and week 1 of a year\n// is the week containing the year's first Thursday (equivalently, the week\n// containing January 4). This means late-December dates can fall in week 1\n// of the *next* year, and early-January dates can fall in week 52 or 53 of\n// the *previous* year. The \"ISO week-numbering year\" (what `RRRR` formats)\n// is that adjacent year, not the calendar year.\n//\n// Computed here from the date's own year/month/day plus its ISO dayOfWeek\n// (1=Mon..7=Sun, matching Temporal's numbering) using plain Gregorian\n// arithmetic — no Temporal factory needed. format() only has the fields\n// the caller already put on the object, and requiring a Temporal\n// implementation just for ISO week would be a regression for callers who\n// use format() without setTemporal() on a non-26 Node.\n\nconst DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\nconst CUMULATIVE_DAYS_BY_MONTH = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];\n\nexport function isGregorianLeapYear(year: number): boolean {\n // Gregorian rule: divisible by 4, except centuries which must also be\n // divisible by 400. Temporal's iso8601 calendar is proleptic Gregorian\n // (no Julian cutover), so this applies for every year, including BCE.\n return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n}\n\nfunction daysInYear(year: number): 365 | 366 {\n return isGregorianLeapYear(year) ? 366 : 365;\n}\n\nexport function dayOfYear(year: number, month: number, day: number): number {\n let doy = CUMULATIVE_DAYS_BY_MONTH[month - 1]! + day;\n if (month > 2 && isGregorianLeapYear(year)) doy += 1;\n return doy;\n}\n\n// Jan 1, 2000 was a Saturday — ISO dayOfWeek 6. Anchoring day-of-week\n// computations to a known reference date is simpler and cheaper than\n// pulling in Zeller's congruence, and the reference never changes.\nconst REFERENCE_YEAR = 2000;\nconst REFERENCE_JAN1_DAY_OF_WEEK = 6;\n\nfunction dayOfWeekOfJan1(year: number): number {\n // Sum full-year deltas from the 2000 anchor rather than recomputing from\n // scratch each call — the per-call work is a single mod this way, and\n // the loop rarely runs far (a typical caller passes a current-era year).\n let offset = 0;\n if (year >= REFERENCE_YEAR) {\n for (let y = REFERENCE_YEAR; y < year; y++) offset += daysInYear(y);\n } else {\n for (let y = year; y < REFERENCE_YEAR; y++) offset -= daysInYear(y);\n }\n // Convert \"days since Jan 1, 2000\" into an ISO day-of-week (1=Mon..7=Sun).\n // Jan 1, 2000 was ISO 6 (Sat), so zero-indexed dow = (6-1 + offset) mod 7.\n const zeroIndexed = (((6 - 1 + offset) % 7) + 7) % 7;\n return zeroIndexed + 1;\n}\n\nexport interface IsoWeekDate {\n isoYear: number;\n week: number; // 1..53\n}\n\nexport function isoWeekYearAndWeek(year: number, month: number, day: number, dayOfWeek: number): IsoWeekDate {\n // Step 1: find the Thursday of the current ISO week. The ISO week-numbering\n // year is whichever calendar year that Thursday falls in. Computing it via\n // day-of-year offsets (rather than constructing a Temporal.PlainDate and\n // adding days) keeps this function pure-numeric.\n const doy = dayOfYear(year, month, day);\n const thursdayDoyRelative = doy + (4 - dayOfWeek); // may be <1 or >daysInYear\n\n let isoYear: number;\n let thursdayDoy: number;\n if (thursdayDoyRelative < 1) {\n isoYear = year - 1;\n thursdayDoy = thursdayDoyRelative + daysInYear(isoYear);\n } else if (thursdayDoyRelative > daysInYear(year)) {\n isoYear = year + 1;\n thursdayDoy = thursdayDoyRelative - daysInYear(year);\n } else {\n isoYear = year;\n thursdayDoy = thursdayDoyRelative;\n }\n\n // Step 2: locate the first Thursday of isoYear — its week is week 1. The\n // first Thursday's day-of-year depends on what weekday Jan 1 of isoYear is.\n const jan1Dow = dayOfWeekOfJan1(isoYear);\n const firstThursdayDoy = 1 + ((4 - jan1Dow + 7) % 7); // 1..7\n\n // Step 3: count full weeks between the two Thursdays.\n const week = 1 + Math.floor((thursdayDoy - firstThursdayDoy) / 7);\n return { isoYear, week };\n}\n"],"mappings":"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","names":["daysInYear","daysInYear"]}
|
package/dist/calendarUtils.d.cts
CHANGED
|
@@ -17,7 +17,17 @@ export declare function isLeapMonth(_view: DateFieldView): boolean;
|
|
|
17
17
|
export declare function dayOfYearHelper(view: DateFieldView): number;
|
|
18
18
|
export declare function weekOfYear(view: DateFieldView): number;
|
|
19
19
|
export declare function weekYear(view: DateFieldView): number;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Fiscal-quarter options. `startMonth` is the calendar month (1-12) the
|
|
22
|
+
* fiscal year begins on — e.g. `7` for a fiscal year starting in July.
|
|
23
|
+
* Omitted or `1` gives the calendar-quarter behavior getQuarter() has
|
|
24
|
+
* always had (Jan-Mar = Q1, etc.), so existing callers passing nothing
|
|
25
|
+
* see no change.
|
|
26
|
+
*/
|
|
27
|
+
export interface QuarterOptions {
|
|
28
|
+
startMonth?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function getQuarter(view: DateFieldView, options?: QuarterOptions): number;
|
|
21
31
|
export declare function getMonth(view: DateFieldView): number;
|
|
22
32
|
export declare function getWeekday(view: DateFieldView): number;
|
|
23
33
|
export type StartOfUnit = 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second';
|
package/dist/calendarUtils.d.ts
CHANGED
|
@@ -17,7 +17,17 @@ export declare function isLeapMonth(_view: DateFieldView): boolean;
|
|
|
17
17
|
export declare function dayOfYearHelper(view: DateFieldView): number;
|
|
18
18
|
export declare function weekOfYear(view: DateFieldView): number;
|
|
19
19
|
export declare function weekYear(view: DateFieldView): number;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Fiscal-quarter options. `startMonth` is the calendar month (1-12) the
|
|
22
|
+
* fiscal year begins on — e.g. `7` for a fiscal year starting in July.
|
|
23
|
+
* Omitted or `1` gives the calendar-quarter behavior getQuarter() has
|
|
24
|
+
* always had (Jan-Mar = Q1, etc.), so existing callers passing nothing
|
|
25
|
+
* see no change.
|
|
26
|
+
*/
|
|
27
|
+
export interface QuarterOptions {
|
|
28
|
+
startMonth?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function getQuarter(view: DateFieldView, options?: QuarterOptions): number;
|
|
21
31
|
export declare function getMonth(view: DateFieldView): number;
|
|
22
32
|
export declare function getWeekday(view: DateFieldView): number;
|
|
23
33
|
export type StartOfUnit = 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second';
|
package/dist/calendarUtils.js
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TOKENS
|
|
3
|
+
} from "./chunk-7SFQUOID.js";
|
|
4
|
+
import {
|
|
5
|
+
InvalidLocaleError
|
|
6
|
+
} from "./chunk-C3755VAP.js";
|
|
7
|
+
|
|
8
|
+
// src/tokenize.ts
|
|
9
|
+
var SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);
|
|
10
|
+
function tokenize(format) {
|
|
11
|
+
const pieces = [];
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < format.length) {
|
|
14
|
+
const ch = format[i];
|
|
15
|
+
if (ch === "'") {
|
|
16
|
+
if (format[i + 1] === "'") {
|
|
17
|
+
appendLiteral(pieces, "'");
|
|
18
|
+
i += 2;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
let j = i + 1;
|
|
22
|
+
let literal = "";
|
|
23
|
+
let closed = false;
|
|
24
|
+
while (j < format.length) {
|
|
25
|
+
if (format[j] === "'") {
|
|
26
|
+
if (format[j + 1] === "'") {
|
|
27
|
+
literal += "'";
|
|
28
|
+
j += 2;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
closed = true;
|
|
32
|
+
j += 1;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
literal += format[j];
|
|
36
|
+
j += 1;
|
|
37
|
+
}
|
|
38
|
+
if (!closed) {
|
|
39
|
+
throw new Error(`temporal-fmt: unterminated quote in format string "${format}"`);
|
|
40
|
+
}
|
|
41
|
+
appendLiteral(pieces, literal);
|
|
42
|
+
i = j;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));
|
|
46
|
+
if (match) {
|
|
47
|
+
const runChar = match[match.length - 1];
|
|
48
|
+
if (format[i + match.length] === runChar) {
|
|
49
|
+
let end = i + match.length;
|
|
50
|
+
while (format[end] === runChar) end += 1;
|
|
51
|
+
throw new Error(
|
|
52
|
+
`temporal-fmt: "${format.slice(i, end)}" in format string "${format}" isn't a recognized token \u2014 did you mean "${match}"?`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
pieces.push({ kind: "token", value: match });
|
|
56
|
+
i += match.length;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
appendLiteral(pieces, ch);
|
|
60
|
+
i += 1;
|
|
61
|
+
}
|
|
62
|
+
return pieces;
|
|
63
|
+
}
|
|
64
|
+
function appendLiteral(pieces, value) {
|
|
65
|
+
const last = pieces[pieces.length - 1];
|
|
66
|
+
if (last && last.kind === "literal") {
|
|
67
|
+
last.value += value;
|
|
68
|
+
} else {
|
|
69
|
+
pieces.push({ kind: "literal", value });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/numbering.ts
|
|
74
|
+
var SUPPORTED_NUMBERING_SYSTEMS = /* @__PURE__ */ new Set([
|
|
75
|
+
"latn",
|
|
76
|
+
"arab",
|
|
77
|
+
"deva",
|
|
78
|
+
"beng",
|
|
79
|
+
"guru",
|
|
80
|
+
"gujr",
|
|
81
|
+
"orya",
|
|
82
|
+
"tamldec",
|
|
83
|
+
"telu",
|
|
84
|
+
"knda",
|
|
85
|
+
"mlym",
|
|
86
|
+
"fullwide",
|
|
87
|
+
"hanidec"
|
|
88
|
+
]);
|
|
89
|
+
var digitMapCache = /* @__PURE__ */ new Map();
|
|
90
|
+
function getDigitMap(system) {
|
|
91
|
+
let map = digitMapCache.get(system);
|
|
92
|
+
if (map) return map;
|
|
93
|
+
if (system === "latn") {
|
|
94
|
+
map = {};
|
|
95
|
+
for (let i = 0; i < 10; i++) map[String(i)] = String(i);
|
|
96
|
+
} else {
|
|
97
|
+
const fmt = new Intl.NumberFormat("en-US-u-nu-" + system, { useGrouping: false });
|
|
98
|
+
map = {};
|
|
99
|
+
for (let i = 0; i < 10; i++) {
|
|
100
|
+
map[String(i)] = fmt.format(i);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
digitMapCache.set(system, map);
|
|
104
|
+
return map;
|
|
105
|
+
}
|
|
106
|
+
function convertDigits(s, system) {
|
|
107
|
+
if (system === "latn") return s;
|
|
108
|
+
if (!SUPPORTED_NUMBERING_SYSTEMS.has(system)) {
|
|
109
|
+
throw new InvalidLocaleError({ actual: system, reason: `numbering system "${system}" is not supported. Supported: ${[...SUPPORTED_NUMBERING_SYSTEMS].join(", ")}.` });
|
|
110
|
+
}
|
|
111
|
+
const map = getDigitMap(system);
|
|
112
|
+
let result = "";
|
|
113
|
+
for (const ch of s) {
|
|
114
|
+
if (ch >= "0" && ch <= "9") {
|
|
115
|
+
result += map[ch] ?? ch;
|
|
116
|
+
} else {
|
|
117
|
+
result += ch;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function convertDigitsToAscii(s, system) {
|
|
123
|
+
if (system === "latn") return s;
|
|
124
|
+
if (!SUPPORTED_NUMBERING_SYSTEMS.has(system)) {
|
|
125
|
+
throw new InvalidLocaleError({ actual: system, reason: `numbering system "${system}" is not supported.` });
|
|
126
|
+
}
|
|
127
|
+
const map = getDigitMap(system);
|
|
128
|
+
const reverse = {};
|
|
129
|
+
for (const k of Object.keys(map)) reverse[map[k]] = k;
|
|
130
|
+
let result = "";
|
|
131
|
+
for (const ch of s) {
|
|
132
|
+
result += reverse[ch] ?? ch;
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
function applyNumbering(s, options) {
|
|
137
|
+
const system = options.numberingSystem ?? "latn";
|
|
138
|
+
if (system === "latn") return s;
|
|
139
|
+
return convertDigits(s, system);
|
|
140
|
+
}
|
|
141
|
+
function applyParseNumbering(s, options) {
|
|
142
|
+
const system = options.parseNumberingSystem ?? "latn";
|
|
143
|
+
if (system === "latn") return s;
|
|
144
|
+
return convertDigitsToAscii(s, system);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
tokenize,
|
|
149
|
+
SUPPORTED_NUMBERING_SYSTEMS,
|
|
150
|
+
convertDigits,
|
|
151
|
+
convertDigitsToAscii,
|
|
152
|
+
applyNumbering,
|
|
153
|
+
applyParseNumbering
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=chunk-26H4NR3J.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tokenize.ts","../src/numbering.ts"],"sourcesContent":["import { TOKENS } from './tokens.js';\n\nexport type Piece =\n | { kind: 'token'; value: string }\n | { kind: 'literal'; value: string };\n\n// longest-first so the greedy scan never matches \"M\" when \"MMMM\" was there\nconst SORTED_TOKEN_STRINGS = TOKENS.map(([tok]) => tok).sort((a, b) => b.length - a.length);\n\n/**\n * Splits a format string like `\"yyyy-MM-dd 'at' HH:mm\"` into token/literal\n * pieces. Text in single quotes is always literal (e.g. write 'rd' in\n * \"3rd\" so it's not read as the day token). A doubled quote ('') means a\n * literal quote character, both inside a quoted span and standalone.\n */\nexport function tokenize(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n\n while (i < format.length) {\n const ch = format[i];\n\n if (ch === \"'\") {\n // check doubled-quote first or \"''best''\" parses wrong\n if (format[i + 1] === \"'\") {\n appendLiteral(pieces, \"'\");\n i += 2;\n continue;\n }\n\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < format.length) {\n if (format[j] === \"'\") {\n if (format[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += format[j];\n j += 1;\n }\n\n if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in format string \"${format}\"`);\n }\n\n appendLiteral(pieces, literal);\n i = j;\n continue;\n }\n\n const match = SORTED_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n // match is already the longest registered token starting at i, so\n // one more of its last character can't be a token of its own —\n // it'd fall through to whatever token or literal rule handles that\n // character next, silently splicing an unrelated field onto this\n // one (e.g. \"zzzz\" used to read as zzz + literal \"z\", \"MMMMM\" as\n // MMMM + the numeric-month token M). Treat the whole overlong run\n // as one unrecognized token instead.\n const runChar = match[match.length - 1];\n if (format[i + match.length] === runChar) {\n let end = i + match.length;\n while (format[end] === runChar) end += 1;\n throw new Error(\n `temporal-fmt: \"${format.slice(i, end)}\" in format string \"${format}\" isn't a recognized token — ` +\n `did you mean \"${match}\"?`\n );\n }\n pieces.push({ kind: 'token', value: match });\n i += match.length;\n continue;\n }\n\n // not a token or quote — pass through as-is\n appendLiteral(pieces, ch);\n i += 1;\n }\n\n return pieces;\n}\n\n// merges into the previous piece if it's also a literal, so \"---\" is one\n// piece instead of three\nfunction appendLiteral(pieces: Piece[], value: string): void {\n const last = pieces[pieces.length - 1];\n if (last && last.kind === 'literal') {\n last.value += value;\n } else {\n pieces.push({ kind: 'literal', value });\n }\n}","// Numbering systems (plan section G). Default: latn (ASCII digits).\n// Optional: arab, deva, beng, etc. Configurable on formatting.\n//\n// The library's existing tokens always render ASCII digits (see\n// tokens.ts's pad() — uses String(n) which produces ASCII). This\n// module adds the ability to convert the output to a locale's native\n// digits via Intl.NumberFormat, which is the standard mechanism JS\n// provides for digit transliteration.\n//\n// On the parse side: parse() only accepts ASCII digits, matching how\n// the existing NUMERIC_FRAGMENTS regex is built. A parseNumberingSystem\n// option could convert input digits to ASCII before matching — but\n// that's a per-call opt-in, not silent acceptance, per the plan's\n// \"explicit semantics rather than silently accepting every numeral\n// system\" instruction.\n\nimport { DEFAULT_LOCALE, type FormatOptions } from './tokens.js';\nimport { InvalidLocaleError } from './errors.js';\n\nexport type NumberingSystem = 'latn' | 'arab' | 'deva' | 'beng' | 'guru' | 'gujr' | 'orya' | 'tamldec' | 'telu' | 'knda' | 'mlym' | 'fullwide' | 'hanidec';\n\n// All NumberingSystem values the plan calls out as supported. latn is\n// the default and what the rest of the library produces natively.\nexport const SUPPORTED_NUMBERING_SYSTEMS: ReadonlySet<string> = new Set([\n 'latn', 'arab', 'deva', 'beng', 'guru', 'gujr', 'orya', 'tamldec',\n 'telu', 'knda', 'mlym', 'fullwide', 'hanidec',\n]);\n\nconst digitMapCache = new Map<string, Record<string, string>>();\n\n// Builds a per-numbering-system digit transliteration map. Uses\n// Intl.NumberFormat to render 0-9 in the requested system, then\n// builds the lookup table. Cached because constructing a formatter\n// is expensive and we re-use the same map for every digit in the\n// output.\nfunction getDigitMap(system: string): Record<string, string> {\n let map = digitMapCache.get(system);\n if (map) return map;\n if (system === 'latn') {\n map = {};\n for (let i = 0; i < 10; i++) map[String(i)] = String(i);\n } else {\n const fmt = new Intl.NumberFormat('en-US-u-nu-' + system, { useGrouping: false });\n map = {};\n for (let i = 0; i < 10; i++) {\n map[String(i)] = fmt.format(i);\n }\n }\n digitMapCache.set(system, map);\n return map;\n}\n\n// Converts every ASCII digit in `s` to its equivalent in the requested\n// numbering system. Non-digit characters pass through unchanged.\nexport function convertDigits(s: string, system: string): string {\n if (system === 'latn') return s;\n if (!SUPPORTED_NUMBERING_SYSTEMS.has(system)) {\n throw new InvalidLocaleError({ actual: system, reason: `numbering system \"${system}\" is not supported. Supported: ${[...SUPPORTED_NUMBERING_SYSTEMS].join(', ')}.` });\n }\n const map = getDigitMap(system);\n let result = '';\n for (const ch of s) {\n if (ch >= '0' && ch <= '9') {\n result += map[ch] ?? ch;\n } else {\n result += ch;\n }\n }\n return result;\n}\n\n// Parses a string with non-ASCII digits back to ASCII. The inverse of\n// convertDigits — used by parse() when an explicit `numberingSystem`\n// option is passed. Throws on unsupported systems.\nexport function convertDigitsToAscii(s: string, system: string): string {\n if (system === 'latn') return s;\n if (!SUPPORTED_NUMBERING_SYSTEMS.has(system)) {\n throw new InvalidLocaleError({ actual: system, reason: `numbering system \"${system}\" is not supported.` });\n }\n const map = getDigitMap(system);\n // Build reverse map.\n const reverse: Record<string, string> = {};\n for (const k of Object.keys(map)) reverse[map[k]!] = k;\n let result = '';\n for (const ch of s) {\n result += reverse[ch] ?? ch;\n }\n return result;\n}\n\n// Augmented FormatOptions that includes the numberingSystem field.\n// Callers pass { numberingSystem: 'arab' } to format() to get Arabic-\n// Indic digits in the output.\nexport interface NumberingFormatOptions extends FormatOptions {\n numberingSystem?: string;\n}\n\n// Parse-side counterpart. Named parseNumberingSystem (not numberingSystem)\n// so a caller mixing format() and parse() options in the same config\n// object can set both independently — the two directions aren't always\n// symmetric (e.g. converting *to* native digits on output without\n// expecting native digits back on input, or vice versa).\nexport interface NumberingParseOptions extends FormatOptions {\n parseNumberingSystem?: string;\n}\n\n// Helper for the format path: takes the formatted ASCII output of\n// format() and converts digits if a numberingSystem was requested.\n// Kept here so format.ts doesn't need to know about numbering systems.\nexport function applyNumbering(s: string, options: NumberingFormatOptions): string {\n const system = options.numberingSystem ?? 'latn';\n if (system === 'latn') return s;\n return convertDigits(s, system);\n}\n\n// Helper for the parse path: converts input digits to ASCII before\n// matching, when parseNumberingSystem is set. Distinct from the format\n// path's option name (numberingSystem vs parseNumberingSystem) so a\n// caller can be explicit about which direction they want transliterated.\nexport function applyParseNumbering(s: string, options: { parseNumberingSystem?: string }): string {\n const system = options.parseNumberingSystem ?? 'latn';\n if (system === 'latn') return s;\n return convertDigitsToAscii(s, system);\n}\n\n// Suppress unused-import warning. DEFAULT_LOCALE is imported for the\n// type augmentation pattern above; if the project ever exports a\n// typed-locale interface, it'll be the source of truth for the default.\nvoid DEFAULT_LOCALE;\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
applyNumbering,
|
|
2
3
|
tokenize
|
|
3
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-26H4NR3J.js";
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_LOCALE,
|
|
6
7
|
TOKENS
|
|
@@ -49,7 +50,7 @@ function format(temporal, formatStr, options = {}) {
|
|
|
49
50
|
}
|
|
50
51
|
result += handler.fn(temporal, locale);
|
|
51
52
|
}
|
|
52
|
-
return result;
|
|
53
|
+
return applyNumbering(result, options);
|
|
53
54
|
}
|
|
54
55
|
function formatToParts(temporal, formatStr, options = {}) {
|
|
55
56
|
if (formatStr.length > MAX_FORMAT_LENGTH) {
|
|
@@ -79,7 +80,7 @@ function formatToParts(temporal, formatStr, options = {}) {
|
|
|
79
80
|
`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)`
|
|
80
81
|
);
|
|
81
82
|
}
|
|
82
|
-
result.push({ type: "token", value: handler.fn(temporal, locale), token: piece.value });
|
|
83
|
+
result.push({ type: "token", value: applyNumbering(handler.fn(temporal, locale), options), token: piece.value });
|
|
83
84
|
}
|
|
84
85
|
return result;
|
|
85
86
|
}
|
|
@@ -110,7 +111,7 @@ function compileFormat(formatStr) {
|
|
|
110
111
|
}
|
|
111
112
|
result += handler.fn(temporal, locale);
|
|
112
113
|
}
|
|
113
|
-
return result;
|
|
114
|
+
return applyNumbering(result, options);
|
|
114
115
|
},
|
|
115
116
|
formatToParts(temporal, options = {}) {
|
|
116
117
|
const locale = options.locale ?? DEFAULT_LOCALE;
|
|
@@ -129,7 +130,7 @@ function compileFormat(formatStr) {
|
|
|
129
130
|
`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)`
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
|
-
out.push({ type: "token", value: handler.fn(temporal, locale), token: piece.value });
|
|
133
|
+
out.push({ type: "token", value: applyNumbering(handler.fn(temporal, locale), options), token: piece.value });
|
|
133
134
|
}
|
|
134
135
|
return out;
|
|
135
136
|
}
|
|
@@ -149,4 +150,4 @@ export {
|
|
|
149
150
|
_getPieces,
|
|
150
151
|
_handlerFor
|
|
151
152
|
};
|
|
152
|
-
//# sourceMappingURL=chunk-
|
|
153
|
+
//# sourceMappingURL=chunk-3ZXF3ELP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/format.ts"],"sourcesContent":["import { TOKENS, DEFAULT_LOCALE, type TemporalLike, type FormatOptions } from './tokens.js';\nimport { tokenize, type Piece } from './tokenize.js';\nimport { MAX_FORMAT_LENGTH } from './constants.js';\nimport { applyNumbering, type NumberingFormatOptions } from './numbering.js';\n\nconst HANDLER_BY_TOKEN = new Map(TOKENS.map(([tok, fn, field]) => [tok, { fn, field }]));\n\n// Pre-tokenized format strings, keyed by (formatStr) — locale doesn't\n// change the tokenization step, only the per-token rendering, so the\n// piece list is shared across locales. Same eviction shape as the\n// other caches in this library.\nconst tokenizeCache = new Map<string, Piece[]>();\nconst MAX_TOKENIZE_CACHE_SIZE = 500;\n\nfunction getPieces(formatStr: string): Piece[] {\n let pieces = tokenizeCache.get(formatStr);\n if (pieces) return pieces;\n if (tokenizeCache.size >= MAX_TOKENIZE_CACHE_SIZE) {\n const oldestKey = tokenizeCache.keys().next().value;\n if (oldestKey !== undefined) tokenizeCache.delete(oldestKey);\n }\n pieces = tokenize(formatStr);\n tokenizeCache.set(formatStr, pieces);\n return pieces;\n}\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 *\n * Throws on a token the input type doesn't support (e.g. 'HH' on a PlainDate).\n */\nexport function format(temporal: TemporalLike, formatStr: string, options: NumberingFormatOptions = {}): string {\n if (formatStr.length > MAX_FORMAT_LENGTH) {\n throw new Error(\n `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters ` +\n `(got ${formatStr.length}).`\n );\n }\n\n const locale = options.locale ?? DEFAULT_LOCALE;\n const pieces = getPieces(formatStr);\n let result = '';\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n if (!handler) {\n // shouldn't happen — tokenize() only emits tokens from TOKENS\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\n }\n\n if (temporal[handler.field] === undefined) {\n throw new Error(\n `temporal-fmt: token \"${piece.value}\" requires \"${handler.field}\", ` +\n `which this Temporal object doesn't have. ` +\n `(e.g. PlainDate has no time fields, PlainTime has no date fields)`\n );\n }\n\n result += handler.fn(temporal, locale);\n }\n\n // Numeral transliteration happens last and only on request — every\n // upstream token handler still emits plain ASCII digits, so this is\n // the single place output digits can diverge from that default.\n return applyNumbering(result, options);\n}\n\n// Shape mirrors Intl.DateTimeFormat.formatToParts: each entry is either\n// a literal (carrying no token info) or a token piece (carrying the\n// token string and the formatted value). Letting callers iterate parts\n// means they can build custom output — strip a token, swap a separator,\n// render each token to its own DOM node — without re-implementing the\n// tokenizer or the field-check logic.\nexport interface FormattedPart {\n type: 'literal' | 'token';\n value: string;\n // Present when `type === 'token'`. Carries the token string (e.g. \"yyyy\")\n // so a caller can look up its metadata via tokenInfo() from analyze.ts.\n token?: string;\n}\n\nexport function formatToParts(temporal: TemporalLike, formatStr: string, options: NumberingFormatOptions = {}): FormattedPart[] {\n if (formatStr.length > MAX_FORMAT_LENGTH) {\n throw new Error(\n `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters ` +\n `(got ${formatStr.length}).`\n );\n }\n const locale = options.locale ?? DEFAULT_LOCALE;\n const pieces = getPieces(formatStr);\n const result: FormattedPart[] = [];\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n // Collapse adjacent literals so formatToParts stays consistent with\n // how format() walks pieces — a multi-char literal \"at \" is one\n // entry, not one per character. Same merge logic as appendLiteral\n // in tokenize.ts.\n const last = result[result.length - 1];\n if (last && last.type === 'literal') {\n last.value += piece.value;\n } else {\n result.push({ type: 'literal', value: piece.value });\n }\n continue;\n }\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n if (!handler) {\n throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\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 // Numeral transliteration applies per-token-part here, rather than\n // once at the end like format() does, so a caller styling individual\n // parts (e.g. one <span> per token) still gets correctly-transliterated\n // digits in each part instead of plain ASCII.\n result.push({ type: 'token', value: applyNumbering(handler.fn(temporal, locale), options), token: piece.value });\n }\n return result;\n}\n\n// Pre-compiles a format string into an object whose format()/formatToParts()\n// methods skip the tokenization step on every call. The tokenizeCache in\n// this module means a plain format(temporal, fmt) call already pays only\n// a Map lookup for tokenization after the first call, so compileFormat()\n// is mostly a typing/ergonomics affordance — useful for callers who want\n// to hold onto a compiled form explicitly (e.g. to inspect the pieces\n// via the .pieces property, or to pass the compiled object around\n// instead of the string).\nexport interface CompiledFormat {\n format(temporal: TemporalLike, options?: NumberingFormatOptions): string;\n formatToParts(temporal: TemporalLike, options?: NumberingFormatOptions): FormattedPart[];\n readonly pieces: ReadonlyArray<Piece>;\n readonly formatStr: string;\n}\n\nexport function compileFormat(formatStr: string): CompiledFormat {\n if (formatStr.length > MAX_FORMAT_LENGTH) {\n throw new Error(\n `temporal-fmt: format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters ` +\n `(got ${formatStr.length}).`\n );\n }\n // Pre-tokenize once. Validation (unknown tokens, unterminated quotes)\n // happens here, not lazily on first format() call — surfaces a bad\n // format string at compile time rather than at first use, which is\n // the point of compiling up front.\n const pieces = getPieces(formatStr);\n return {\n formatStr,\n pieces,\n format(temporal: TemporalLike, options: NumberingFormatOptions = {}) {\n const locale = options.locale ?? DEFAULT_LOCALE;\n let result = '';\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n result += piece.value;\n continue;\n }\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n if (!handler) throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\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 result += handler.fn(temporal, locale);\n }\n return applyNumbering(result, options);\n },\n formatToParts(temporal: TemporalLike, options: NumberingFormatOptions = {}) {\n const locale = options.locale ?? DEFAULT_LOCALE;\n const out: FormattedPart[] = [];\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n const last = out[out.length - 1];\n if (last && last.type === 'literal') last.value += piece.value;\n else out.push({ type: 'literal', value: piece.value });\n continue;\n }\n const handler = HANDLER_BY_TOKEN.get(piece.value);\n if (!handler) throw new Error(`temporal-fmt: unknown token \"${piece.value}\"`);\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 out.push({ type: 'token', value: applyNumbering(handler.fn(temporal, locale), options), token: piece.value });\n }\n return out;\n },\n };\n}\n\n// Exported so analyze.ts can reuse the same tokenization cache rather\n// than re-tokenizing when a caller asks for both a format() and an\n// analyzeFormat() on the same string.\nexport function _getPieces(formatStr: string): Piece[] {\n return getPieces(formatStr);\n}\n\n// Exported for analyze.ts — same reason as above. The handler map is\n// the source of truth for \"what field does this token need\" — analyze\n// consumes it to compute requiredFields and compatibleTypes.\nexport function _handlerFor(token: string): { fn: (t: TemporalLike, locale: string) => string; field: keyof TemporalLike } | undefined {\n return HANDLER_BY_TOKEN.get(token);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
format,
|
|
3
3
|
formatToParts
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-3ZXF3ELP.js";
|
|
5
5
|
import {
|
|
6
6
|
compare
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-RHGGZMLN.js";
|
|
8
8
|
import {
|
|
9
9
|
asDateFieldView
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-ASLNQ4DG.js";
|
|
11
11
|
|
|
12
12
|
// src/interval.ts
|
|
13
13
|
function interval(start, end, bounds = "closed") {
|
|
@@ -194,4 +194,4 @@ export {
|
|
|
194
194
|
formatRange,
|
|
195
195
|
formatRangeToParts
|
|
196
196
|
};
|
|
197
|
-
//# sourceMappingURL=chunk-
|
|
197
|
+
//# sourceMappingURL=chunk-46IMIRAG.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
add
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-AC66SDAS.js";
|
|
4
4
|
import {
|
|
5
5
|
compare
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-RHGGZMLN.js";
|
|
7
7
|
|
|
8
8
|
// src/recurrence.ts
|
|
9
9
|
function recurrence(start, rule) {
|
|
@@ -173,4 +173,4 @@ export {
|
|
|
173
173
|
parseRRule,
|
|
174
174
|
formatRRule
|
|
175
175
|
};
|
|
176
|
-
//# sourceMappingURL=chunk-
|
|
176
|
+
//# sourceMappingURL=chunk-6EB3FIZL.js.map
|