temporal-fmt 0.9.1 → 0.9.3
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 +166 -1
- package/dist/chunk-2JHEV7ZF.js +7 -0
- package/dist/chunk-2JHEV7ZF.js.map +1 -0
- package/dist/chunk-4JDAJSEM.js +35 -0
- package/dist/chunk-4JDAJSEM.js.map +1 -0
- package/dist/chunk-4XRE37WT.js +2 -0
- package/dist/chunk-4XRE37WT.js.map +1 -0
- package/dist/{chunk-H52AMABK.js → chunk-5U5WJ465.js} +6 -6
- package/dist/chunk-5U5WJ465.js.map +1 -0
- package/dist/chunk-GMGZZG6I.js +2 -0
- package/dist/chunk-GMGZZG6I.js.map +1 -0
- package/dist/{chunk-M5WL5VGW.js → chunk-NPKJ7QFK.js} +2 -2
- package/dist/chunk-R4YEFOVE.js +2 -0
- package/dist/chunk-R4YEFOVE.js.map +1 -0
- package/dist/{chunk-DXLBLPLQ.js → chunk-SSPABEUR.js} +2 -2
- package/dist/chunk-UJR6DHXY.js +2 -0
- package/dist/chunk-UJR6DHXY.js.map +1 -0
- package/dist/{chunk-6UNSCZPL.js → chunk-XH46JNPK.js} +2 -2
- package/dist/chunk-XH46JNPK.js.map +1 -0
- package/dist/chunk-ZGMTY52O.js +2 -0
- package/dist/chunk-ZGMTY52O.js.map +1 -0
- package/dist/duration.cjs +4 -10
- package/dist/duration.cjs.map +1 -1
- package/dist/duration.js +1 -1
- package/dist/format.cjs +6 -6
- package/dist/format.cjs.map +1 -1
- package/dist/format.js +1 -1
- package/dist/formatDuration.d.cts +6 -0
- package/dist/formatDuration.d.ts +6 -0
- package/dist/index.cjs +37 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -15
- package/dist/index.js.map +1 -1
- package/dist/interval.cjs +6 -6
- package/dist/interval.cjs.map +1 -1
- package/dist/interval.js +1 -1
- package/dist/localeRegistry.cjs +4 -4
- package/dist/localeRegistry.cjs.map +1 -1
- package/dist/localeRegistry.js +1 -1
- package/dist/localeVocab.d.cts +3 -0
- package/dist/localeVocab.d.ts +3 -0
- package/dist/parse.cjs +9 -9
- package/dist/parse.cjs.map +1 -1
- package/dist/parse.js +1 -1
- package/dist/parsePattern.d.cts +1 -0
- package/dist/parsePattern.d.ts +1 -0
- package/dist/pattern.d.cts +2 -0
- package/dist/pattern.d.ts +2 -0
- package/dist/recurrence.cjs +4 -4
- package/dist/recurrence.cjs.map +1 -1
- package/dist/recurrence.js +1 -1
- package/dist/relativeTime.cjs +24 -1
- package/dist/relativeTime.cjs.map +1 -1
- package/dist/relativeTime.js +1 -1
- package/dist/rounding.d.cts +1 -0
- package/dist/rounding.d.ts +1 -0
- package/dist/serialization.d.cts +1 -1
- package/dist/serialization.d.ts +1 -1
- package/dist/typeGuards.d.cts +9 -9
- package/dist/typeGuards.d.ts +9 -9
- package/package.json +1 -1
- package/dist/chunk-6JAWE65A.js +0 -2
- package/dist/chunk-6JAWE65A.js.map +0 -1
- package/dist/chunk-6UNSCZPL.js.map +0 -1
- package/dist/chunk-CUTVOO4Y.js +0 -35
- package/dist/chunk-CUTVOO4Y.js.map +0 -1
- package/dist/chunk-H52AMABK.js.map +0 -1
- package/dist/chunk-ILXRFTAE.js +0 -13
- package/dist/chunk-ILXRFTAE.js.map +0 -1
- package/dist/chunk-LYWOLBMV.js +0 -2
- package/dist/chunk-LYWOLBMV.js.map +0 -1
- package/dist/chunk-QASXSTBN.js +0 -2
- package/dist/chunk-QASXSTBN.js.map +0 -1
- package/dist/chunk-YAVXHM4W.js +0 -2
- package/dist/chunk-YAVXHM4W.js.map +0 -1
- package/dist/chunk-ZYVMZRMR.js +0 -2
- package/dist/chunk-ZYVMZRMR.js.map +0 -1
- /package/dist/{chunk-M5WL5VGW.js.map → chunk-NPKJ7QFK.js.map} +0 -0
- /package/dist/{chunk-DXLBLPLQ.js.map → chunk-SSPABEUR.js.map} +0 -0
package/README.md
CHANGED
|
@@ -997,7 +997,172 @@ Neither of these ships as part of this repository — separate packages, install
|
|
|
997
997
|
|
|
998
998
|
## Testing
|
|
999
999
|
|
|
1000
|
-
This library is heavily tested. The `node:test` suite (`test/*.test.js`) runs
|
|
1000
|
+
This library is heavily tested. The `node:test` suite (`test/*.test.js`) runs 1300+ cases covering hand-picked scenarios, fuzzing, and adversarial input, alongside a separate `vitest/` suite unit-testing internals directly. On top of that there's a dedicated conformance suite, smoke tests that check the package actually resolves correctly under CJS/ESM/bundler/nodenext, and type tests. If it's mentioned in this README, it's backed by a test — not just a docstring.
|
|
1001
|
+
|
|
1002
|
+
# Conformance fixtures
|
|
1003
|
+
|
|
1004
|
+
`fixtures.json` is a portable, library-agnostic test-vector set for
|
|
1005
|
+
token-based Temporal formatters. It's written against a different
|
|
1006
|
+
library's token vocabulary, not temporal-fmt's — the fixtures are
|
|
1007
|
+
data, not code. Each case names an `op` (`format` / `parse` /
|
|
1008
|
+
`roundtrip`), an input, a pattern, and an expected result, so any
|
|
1009
|
+
library with a `format`/`parse` pair can be pointed at it.
|
|
1010
|
+
|
|
1011
|
+
`test/conformance.test.js` is the temporal-fmt-specific adapter. It
|
|
1012
|
+
translates fixture patterns into temporal-fmt's actual tokens, runs
|
|
1013
|
+
the cases against `format()`/`parse()`, and checks the result.
|
|
1014
|
+
|
|
1015
|
+
## Why this is separate from `test/adversarial.test.js` and `test/fuzz.test.js`
|
|
1016
|
+
|
|
1017
|
+
Those two check that temporal-fmt is internally consistent under
|
|
1018
|
+
hostile input — clean throw or correct value, never a crash or a
|
|
1019
|
+
silently wrong one. The reference point there is the library's own
|
|
1020
|
+
logic.
|
|
1021
|
+
|
|
1022
|
+
This folder is different: it checks temporal-fmt against an external,
|
|
1023
|
+
shared set of tricky-but-well-defined cases — DST transitions, leap
|
|
1024
|
+
years, offset rendering, calendar limits — where "correct" comes from
|
|
1025
|
+
the fixture, not from temporal-fmt's own code.
|
|
1026
|
+
|
|
1027
|
+
## Pattern translation
|
|
1028
|
+
|
|
1029
|
+
Two fixture tokens don't exist in temporal-fmt:
|
|
1030
|
+
|
|
1031
|
+
| Fixture token | temporal-fmt equivalent | Why |
|
|
1032
|
+
|---|---|---|
|
|
1033
|
+
| `ZZ` (always-signed offset, never `Z`) | `xxx` | Only the **uppercase** `X`/`XX`/`XXX` family collapses `+00:00` to `Z` (see `formatOffset()` in `src/tokens.ts`). Lowercase never does, which is exactly `ZZ`'s semantics. Mapping `ZZ` to `XXX` was tried first and is wrong — it fails `offset-ZZ-format-utc-not-Z` and `zone-utc-roundtrip`, both of which expect `+00:00`, not `Z`. |
|
|
1034
|
+
| `VV` (IANA zone id) | `zzz` | temporal-fmt's only zone-identity token. |
|
|
1035
|
+
|
|
1036
|
+
Translation happens in `translatePattern()` and skips anything inside
|
|
1037
|
+
a quoted literal span. Everything else in the fixture set — `yyyy`,
|
|
1038
|
+
`y`, `MM`, `dd`, `HH`, `mm`, `ss`, `S`..`SSSSSSSSS`, `h`, `a`,
|
|
1039
|
+
`X`/`XX`/`XXX` — already matches temporal-fmt's vocabulary directly.
|
|
1040
|
+
|
|
1041
|
+
## `opinionated` cases
|
|
1042
|
+
|
|
1043
|
+
Some cases are flagged `"opinionated": true` right in the fixture.
|
|
1044
|
+
These encode a design choice of the fixture's source library, not a
|
|
1045
|
+
fact about dates, and temporal-fmt is allowed to disagree with them.
|
|
1046
|
+
The adapter still runs them — if temporal-fmt's behavior differs, it
|
|
1047
|
+
logs a divergence note (printed at the end of the run) instead of
|
|
1048
|
+
failing the suite.
|
|
1049
|
+
|
|
1050
|
+
Two cases currently diverge, both `yy`-pivot ones —
|
|
1051
|
+
`extreme-year-two-digit-pivot-low` and
|
|
1052
|
+
`extreme-year-two-digit-pivot-high`. temporal-fmt refuses `yy` in any
|
|
1053
|
+
format string that isn't a complete date (`yyyy`/`yy` + month + day),
|
|
1054
|
+
so `parse("yy-MM", ...)` throws an incomplete-date error before the
|
|
1055
|
+
question of *which* century a 2-digit year should resolve to ever
|
|
1056
|
+
comes up. The fixture's position — that bare `yy-MM` should resolve
|
|
1057
|
+
via the 00-68/69-99 ECMAScript pivot — is a convention, not a fact
|
|
1058
|
+
about dates; a library is free to pick a different pivot, or, as here,
|
|
1059
|
+
decline to guess a century from `yy` alone at all. Both are documented
|
|
1060
|
+
design choices with their own passing tests
|
|
1061
|
+
(`test/parse.test.js`, `yy pivot: ...`), not something in scope to
|
|
1062
|
+
"fix" by adopting the fixture's convention.
|
|
1063
|
+
|
|
1064
|
+
One other flagged case no longer diverges:
|
|
1065
|
+
**`shape-mixing-H-and-a-rejected`**. `resolveHour()`
|
|
1066
|
+
(`src/parse.ts`) used to cross-check `H` against `a` instead of
|
|
1067
|
+
banning the combination outright, so `13:05 PM` was accepted (13:00 is
|
|
1068
|
+
consistent with PM) and only a genuine contradiction like `01:05 PM`
|
|
1069
|
+
threw. That choice has since been reverted — `H` and `a` are now
|
|
1070
|
+
refused together outright, unconditionally, matching the fixture. The
|
|
1071
|
+
fixture's own `"opinion"` text on that case still describes the old
|
|
1072
|
+
behavior; it's fixture data, not something this adapter edits, so
|
|
1073
|
+
treat the `opinionated` flag there as historical rather than current.
|
|
1074
|
+
|
|
1075
|
+
## History: divergences that have since been fixed
|
|
1076
|
+
|
|
1077
|
+
Everything below was once tracked in `KNOWN_FAILURES` at the top of
|
|
1078
|
+
`test/conformance.test.js`. That set is currently empty — every
|
|
1079
|
+
previously-found divergence has been resolved, either by fixing a
|
|
1080
|
+
real bug or by deliberately adopting the fixture's convention over a
|
|
1081
|
+
prior design choice. Kept here for context on what changed and why,
|
|
1082
|
+
in case any of it needs revisiting.
|
|
1083
|
+
|
|
1084
|
+
**Fixed — real bug: offset seconds were dropped, not rejected.**
|
|
1085
|
+
`formatOffset()` (`src/tokens.ts`) assumed every offset string was
|
|
1086
|
+
exactly 6 characters — sign, `HH`, `:`, `MM` — and never checked for a
|
|
1087
|
+
seconds component. Verified against a real `Temporal.ZonedDateTime`
|
|
1088
|
+
for a pre-1900 `America/New_York` date: the actual offset is
|
|
1089
|
+
`-04:56:02`, 9 characters, because pre-1883 New York ran on local mean
|
|
1090
|
+
time. The old code read that string's middle two digits as minutes,
|
|
1091
|
+
so `X` silently produced `-0402` (wrong) instead of refusing. Now:
|
|
1092
|
+
`X`/`XX`/`XXX`/`x`/`xx` throw when the offset has a seconds component
|
|
1093
|
+
(none of them have anywhere to put it), and `xxx` — the variant that
|
|
1094
|
+
plays `ZZ`'s "always-signed, never-Z" role — passes the full value
|
|
1095
|
+
through unchanged.
|
|
1096
|
+
- `offset-sub-minute-rejected-by-X`
|
|
1097
|
+
- `offset-sub-minute-passes-through-ZZ`
|
|
1098
|
+
|
|
1099
|
+
**Changed — offset-only `ZonedDateTime` construction, previously
|
|
1100
|
+
supported on purpose, is now refused.** `parse()` used to build a
|
|
1101
|
+
`ZonedDateTime` from an offset token alone, no `zzz` zone required.
|
|
1102
|
+
That was deliberate, not an oversight, but the fixture's position — an
|
|
1103
|
+
offset identifies a moment's distance from UTC, not a time zone, so
|
|
1104
|
+
building a `ZonedDateTime` from one alone papers over that distinction
|
|
1105
|
+
— was adopted instead. A pattern with an offset token and no `zzz` now
|
|
1106
|
+
throws; add `zzz` to the pattern (or parse into a
|
|
1107
|
+
`PlainDateTime`/`PlainDate`/`PlainTime` if a zone genuinely isn't
|
|
1108
|
+
needed).
|
|
1109
|
+
- `zone-required-for-zoneddatetime`
|
|
1110
|
+
- `zone-offset-token-rejected-on-plain-type`
|
|
1111
|
+
|
|
1112
|
+
**Added — `y` token (unpadded, variable-width year).** temporal-fmt
|
|
1113
|
+
previously had only `yyyy` (fixed 4 digits) and `yy` (2-digit,
|
|
1114
|
+
truncated). `y` formats and parses a year at any width, sign preserved
|
|
1115
|
+
for years before ISO year 0 — same semantics as `yyyy` minus the
|
|
1116
|
+
fixed width. It has no bounded fallback the way `yyyy` does when
|
|
1117
|
+
something digit-consuming follows (`yyyy` can fall back to an exact
|
|
1118
|
+
4-digit fragment in that case; `y` being unpadded is the entire point
|
|
1119
|
+
of the token, so there's no narrower shape that still means the same
|
|
1120
|
+
thing). `buildCapturingPattern()` (`src/parsePattern.ts`) refuses at
|
|
1121
|
+
build time to place `y` directly next to another digit-reading token
|
|
1122
|
+
or a digit-leading literal, rather than trying to estimate an
|
|
1123
|
+
ambiguity cost for an unbounded-width fragment — there's no finite
|
|
1124
|
+
number of "width choices" to charge for "any number of digits."
|
|
1125
|
+
- `extreme-year-max-supported`
|
|
1126
|
+
- `extreme-year-negative`
|
|
1127
|
+
- `extreme-year-past-max-rejected` — previously passed even without a
|
|
1128
|
+
real `y` token, because "no valid pattern matches" for the
|
|
1129
|
+
then-unrecognized token happened to also throw. Now genuinely tests
|
|
1130
|
+
275761 CE rejection, via the real max-year check on `y`'s parsed
|
|
1131
|
+
value.
|
|
1132
|
+
|
|
1133
|
+
**Fixed — misdiagnosed as a regex gap; the actual cause was `zzz`
|
|
1134
|
+
rejecting valid IANA zone aliases.** `offset-X-parse-accepts-four-digit`
|
|
1135
|
+
expects `X` to parse a 4-digit offset body (`+0530`) alongside a `zzz`
|
|
1136
|
+
zone. This was originally filed as "the capturing regex for `X` in
|
|
1137
|
+
`pattern.ts` doesn't offer the 4-digit shape as an alternative" — that
|
|
1138
|
+
diagnosis was wrong. The `X` regex fragment matches `+0530` correctly
|
|
1139
|
+
in isolation; the actual failure was `isValidTimeZone()`
|
|
1140
|
+
(`src/pattern.ts`) rejecting the fixture's zone name, `Asia/Kolkata`.
|
|
1141
|
+
`isValidTimeZone()` only checked `Intl.supportedValuesOf('timeZone')`,
|
|
1142
|
+
which lists canonical zone ids but not every IANA link/alias name —
|
|
1143
|
+
`Asia/Kolkata` is a legitimate, commonly-used alias for
|
|
1144
|
+
`Asia/Calcutta` that some ICU builds' `supportedValuesOf()` omits.
|
|
1145
|
+
Confirmed against `temporal-polyfill` directly:
|
|
1146
|
+
`Temporal.ZonedDateTime.from()` resolves `Asia/Kolkata` without
|
|
1147
|
+
complaint, so `parse()` was refusing input its own downstream
|
|
1148
|
+
construction step would have accepted. `isValidTimeZone()` now falls
|
|
1149
|
+
back to asking `Temporal` itself (a real `ZonedDateTime.from()` call)
|
|
1150
|
+
when the fast-path `Intl` lookup misses, rather than trusting only the
|
|
1151
|
+
`Intl` list.
|
|
1152
|
+
|
|
1153
|
+
## Adapter mapping notes
|
|
1154
|
+
|
|
1155
|
+
- `parse(formatStr, input, options)` takes `(formatStr, input)` —
|
|
1156
|
+
reversed from the fixture's `pattern`/`input` field order.
|
|
1157
|
+
- temporal-fmt does have a typed error hierarchy (`TemporalFmtError`
|
|
1158
|
+
and its subclasses in `src/errors.ts`), but the fixture's three
|
|
1159
|
+
`expect.throws` values (`"ParseError"`, `"FormatError"`,
|
|
1160
|
+
`"InvalidPatternError"`) don't map cleanly onto temporal-fmt's
|
|
1161
|
+
dozen-plus subclasses, so the adapter doesn't try — it just checks
|
|
1162
|
+
that something extending `Error` was thrown.
|
|
1163
|
+
- `target` in the fixture is informational only. temporal-fmt's
|
|
1164
|
+
`parse()` infers the result shape from which fields the pattern
|
|
1165
|
+
captures, so the adapter never passes `target` as an input.
|
|
1001
1166
|
|
|
1002
1167
|
## Contributing
|
|
1003
1168
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{b as F,k as I,l as k,p as U,q as S}from"./chunk-5U5WJ465.js";import{n as v}from"./chunk-EHLRZULM.js";var h={day:864e5,hour:36e5,minute:6e4,second:1e3,millisecond:1};function E(n,t,e){let o=n<0?-1:1,i=Math.abs(n)/e,s;switch(t){case"nearest":s=Math.round(i);break;case"floor":s=Math.floor(i);break;case"ceil":s=Math.ceil(i);break;case"trunc":s=Math.trunc(i);break}return o*s*e}function A(n){let t=n.year,e=n.month,o=n.day,r=e<=2?t-1:t,i=Math.floor((r>=0?r:r-399)/400),s=r-i*400,a=e>2?e-3:e+9,u=Math.floor((153*a+2)/5)+o-1,c=s*365+Math.floor(s/4)-Math.floor(s/100)+u;return(i*146097+c-719468)*h.day+(n.hour??0)*h.hour+(n.minute??0)*h.minute+(n.second??0)*h.second+(n.millisecond??0)}function V(n,t){let e=h.day,o=Math.floor(n/e),r=n-o*e;r<0&&(r+=e);let i=Math.floor(r/h.hour),s=Math.floor(r%h.hour/h.minute),a=Math.floor(r%h.minute/h.second),u=r%h.second,c=o+719468,l=Math.floor((c>=0?c:c-146096)/146097),m=c-l*146097,d=Math.floor((m-Math.floor(m/1460)+Math.floor(m/36524)-Math.floor(m/146096))/365),D=d+l*400,g=m-(365*d+Math.floor(d/4)-Math.floor(d/100)),y=Math.floor((5*g+2)/153),f=g-Math.floor((153*y+2)/5)+1,p=y<10?y+3:y-9,_=p<=2?D+1:D;return{...t,year:_,month:p,day:f,hour:i,minute:s,second:a,millisecond:u}}function P(n,t){let e=v(n),o=t.mode??"nearest",r=(t.roundingIncrement??1)*h[t.unit];if(r<=0)throw new Error(`temporal-fmt: round() requires a positive roundingIncrement (got ${t.roundingIncrement}).`);let i=A(e),s=E(i,o,r);return V(s,e)}function J(n,t,e=1){return P(n,{unit:t,mode:"floor",roundingIncrement:e})}function Q(n,t,e=1){return P(n,{unit:t,mode:"ceil",roundingIncrement:e})}function nn(n,t,e=1){return P(n,{unit:t,mode:"trunc",roundingIncrement:e})}function b(n,t,e){if(Number.isInteger(n))return BigInt(n)*t;let o=n*Number(t);if(Number.isSafeInteger(o))return BigInt(o);throw new k({reason:`field "${e}" (${n}) cannot be converted to nanoseconds exactly \u2014 the value is too large for a fractional unit; use smaller units`})}var M={years:0n,months:0n,weeks:0n,days:86400n*1000000000n,hours:3600n*1000000000n,minutes:60n*1000000000n,seconds:1000000000n,milliseconds:1000000n,microseconds:1000n,nanoseconds:1n};function j(n){return n==="years"||n==="months"||n==="weeks"}function z(n,t){let e=t.mode??"nearest",o=BigInt(t.roundingIncrement??1);if(o<=0n)throw new Error(`temporal-fmt: roundDuration() requires a positive roundingIncrement (got ${t.roundingIncrement}).`);if(j(t.unit))throw new Error(`temporal-fmt: roundDuration() to "${t.unit}" requires a Temporal.Duration with a relativeTo \u2014 this helper operates on plain field bags without calendar context. Use Temporal.Duration.prototype.round() directly.`);let r=0n;for(let f of["days","hours","minutes","seconds","milliseconds","microseconds","nanoseconds"]){let p=n[f]??0;r+=b(p,M[f],f)}let i=o*M[t.unit],s=r<0n?-1n:1n,a=r<0n?-r:r,u=a/i,c=a%i,l;switch(e){case"nearest":l=c*2n>=i?u+1n:u;break;case"floor":l=u;break;case"ceil":l=c>0n?u+1n:u;break;case"trunc":l=u;break}let m=s*l*i,d={...n},D=m,g=["days","hours","minutes","seconds","milliseconds","microseconds","nanoseconds"],y=g.indexOf(t.unit);for(let f=y+1;f<g.length;f++)d[g[f]]=0;for(let f=0;f<=y;f++){let p=g[f],_=M[p];if(_===0n)continue;let T=D/_;D-=T*_,d[p]=Number(T)}return d}var $={y:{longSingular:"year",longPlural:"years",shortSingular:"yr",shortPlural:"yrs",field:"years",intlUnit:"year"},o:{longSingular:"month",longPlural:"months",shortSingular:"mo",shortPlural:"mos",field:"months",intlUnit:"month"},w:{longSingular:"week",longPlural:"weeks",shortSingular:"wk",shortPlural:"wks",field:"weeks",intlUnit:"week"},d:{longSingular:"day",longPlural:"days",shortSingular:"d",shortPlural:"d",field:"days",intlUnit:"day"},h:{longSingular:"hour",longPlural:"hours",shortSingular:"h",shortPlural:"h",field:"hours",intlUnit:"hour"},m:{longSingular:"minute",longPlural:"minutes",shortSingular:"m",shortPlural:"m",field:"minutes",intlUnit:"minute"},s:{longSingular:"second",longPlural:"seconds",shortSingular:"s",shortPlural:"s",field:"seconds",intlUnit:"second"},S:{longSingular:"millisecond",longPlural:"milliseconds",shortSingular:"ms",shortPlural:"ms",field:"milliseconds",intlUnit:"millisecond"}},L=Object.keys($).flatMap(n=>[n+n+n,n+n,n]).sort((n,t)=>t.length-n.length);function q(n){let t=[],e=0;for(;e<n.length;){let o=n[e];if(o==="'"){if(n[e+1]==="'"){N(t,"'"),e+=2;continue}let i=e+1,s="",a=!1;for(;i<n.length;){if(n[i]==="'"){if(n[i+1]==="'"){s+="'",i+=2;continue}a=!0,i+=1;break}s+=n[i],i+=1}if(!a)throw new Error(`temporal-fmt: unterminated quote in duration format string "${n}"`);N(t,s),e=i;continue}let r=L.find(i=>n.startsWith(i,e));if(r){let i=r[0],s=r.length===1?"numeric":r.length===2?"short":"long";t.push({kind:"token",value:r,unit:i,form:s}),e+=r.length;continue}N(t,o),e+=1}return t}function N(n,t){let e=n[n.length-1];e&&e.kind==="literal"?e.value+=t:n.push({kind:"literal",value:t})}function C(n,t){let e=n[t];if(e==null)return 0;if(typeof e=="number"&&Number.isFinite(e))return e;let o=Number(e);if(!Number.isFinite(o))throw new Error(`temporal-fmt: duration field "${t}" is not a finite number (got ${String(e)}).`);return o}var w=new Map,H=200;function W(n,t,e){let o=`${S(n)}|${t}|${e}`,r=w.get(o);if(r)return r;if(w.size>=H){let s=w.keys().next().value;s!==void 0&&w.delete(s)}let i;try{i=new Intl.NumberFormat(U(n),{style:"unit",unit:t,unitDisplay:e})}catch(s){throw new I({actual:n,reason:s.message})}return w.set(o,i),i}function rn(n,t,e={}){return x(n,t,e).map(o=>o.value).join("")}function x(n,t,e={}){if(t.length>1e3)throw new Error(`temporal-fmt: duration format string exceeds maximum length of ${1e3} characters (got ${t.length}).`);let o=e.showZeroValues===!0,r=e.locale,i=r!==void 0,s=q(t),a=[];for(let u of s){if(u.kind==="literal"){a.push({kind:"literal",value:u.value});continue}let c=$[u.unit];if(!c)throw new Error(`temporal-fmt: unknown duration token "${u.value}"`);let l=C(n,c.field);if(l===0&&!o){a.push({kind:"token",value:"",token:u.value});continue}if(u.form==="numeric"){a.push({kind:"token",value:String(l),token:u.value});continue}if(i){let m=u.form==="short"?"short":"long",d=W(r,c.intlUnit,m);a.push({kind:"token",value:d.format(l),token:u.value});continue}u.form==="short"?a.push({kind:"token",value:l+(l===1||l===-1?c.shortSingular:c.shortPlural),token:u.value}):a.push({kind:"token",value:l+" "+(l===1||l===-1?c.longSingular:c.longPlural),token:u.value})}return a}function dn(n,t,e={}){return x(n,t,e).map(o=>o.kind==="token"?{type:"token",value:o.value,token:o.token}:{type:"literal",value:o.value})}function mn(n){let e=/^P(?:(?:(\d+(?:\.\d+)?)Y)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(n);if(!e)throw new k({input:n,reason:"does not match ISO 8601 duration grammar (P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S)"});if(e.slice(1).every(r=>r===void 0))throw new k({input:n,reason:'duration has no fields (use "P0D" for zero duration)'});let o=r=>r===void 0?0:Number(r);return{years:o(e[1]),months:o(e[2]),weeks:o(e[3]),days:o(e[4]),hours:o(e[5]),minutes:o(e[6]),seconds:o(e[7])}}function fn(n){let t=["P"],e=n.years??0,o=n.months??0,r=n.weeks??0,i=n.days??0,s=n.hours??0,a=n.minutes??0,u=n.seconds??0;return e&&t.push(`${e}Y`),o&&t.push(`${o}M`),r&&t.push(`${r}W`),i&&t.push(`${i}D`),(s||a||u)&&(t.push("T"),s&&t.push(`${s}H`),a&&t.push(`${a}M`),u&&t.push(`${u}S`)),t.length===1&&t.push("0D"),t.join("")}function B(n){let t=["y","o","w","d","h","m","s","S"].flatMap(r=>[r+r+r,r+r,r]).sort((r,i)=>i.length-r.length),e=[],o=0;for(;o<n.length;){let r=n[o];if(r==="'"){if(n[o+1]==="'"){R(e,"'"),o+=2;continue}let s=o+1,a="",u=!1;for(;s<n.length;){if(n[s]==="'"){if(n[s+1]==="'"){a+="'",s+=2;continue}u=!0,s+=1;break}a+=n[s],s+=1}if(!u)throw new F({format:n,reason:"unterminated quote"});R(e,a),o=s;continue}let i=t.find(s=>n.startsWith(s,o));if(i){e.push({kind:"token",value:i,unit:i[0]}),o+=i.length;continue}R(e,r),o+=1}return e}function R(n,t){let e=n[n.length-1];e&&e.kind==="literal"?e.value+=t:n.push({kind:"literal",value:t})}function hn(n,t,e={}){let o=B(t),r={},i=[],s="";for(let c of o)if(c.kind==="literal")s+=c.value.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");else{let l=c.unit,m=c.value.length===1?"numeric":c.value.length===2?"short":"long";/* c8 ignore start @preserve -- the final `: null` branch, and the
|
|
2
|
+
`if (key === null)` throw it feeds, are both unreachable: tok.unit
|
|
3
|
+
is always one of the 8 letters in DURATION_TOKEN_STRINGS (the same
|
|
4
|
+
source set this ternary checks against), so `null` can't be
|
|
5
|
+
produced by any token the tokenizer emits. Kept as a guard against
|
|
6
|
+
a future letter being added to one set without the other. */let d=l==="y"?"years":l==="o"?"months":l==="w"?"weeks":l==="d"?"days":l==="h"?"hours":l==="m"?"minutes":l==="s"?"seconds":l==="S"?"milliseconds":null;if(d===null)throw new F({format:t,token:c.value,reason:"unknown duration token"});/* c8 ignore stop @preserve */m==="numeric"?s+="(-?\\d+)":s+="(\\d+)\\s*\\w*",i.push({unit:d,form:m})}let u=new RegExp(`^${s}$`,"u").exec(n);if(!u)throw new F({input:n,format:t,reason:"does not match duration format"});return i.forEach((c,l)=>{let m=u[l+1],d=Number(m);if(!Number.isFinite(d))throw new k({input:n,reason:`token for ${c.unit} matched "${m}" but isn't a finite number`});r[c.unit]=d}),r}function gn(n){let t=[["days",86400n*1000000000n],["hours",3600n*1000000000n],["minutes",60n*1000000000n],["seconds",1000000000n],["milliseconds",1000000n],["microseconds",1000n],["nanoseconds",1n]],e=0n;for(let[i,s]of t){let a=n[i];typeof a=="number"&&(e+=b(a,s,i))}let o={...n},r=e;for(let[i,s]of t){let a=r/s;r-=a*s,o[i]=Number(a)}return o}function yn(n,t){let e={days:86400n*1000000000n,hours:3600n*1000000000n,minutes:60n*1000000000n,seconds:1000000000n,milliseconds:1000000n,microseconds:1000n,nanoseconds:1n},o=0n;for(let i of Object.keys(e)){let s=n[i];typeof s=="number"&&(o+=b(s,e[i],i))}let r=e[t];if(!r)throw new k({reason:`totalDuration() does not support unit "${t}" (calendar-bound units need a relativeTo)`});return Number(o)/Number(r)}function pn(n,t){let e=O(n),o=O(t);return e<o?-1:e>o?1:0}function O(n){let t={days:86400n*1000000000n,hours:3600n*1000000000n,minutes:60n*1000000000n,seconds:1000000000n,milliseconds:1000000n,microseconds:1000n,nanoseconds:1n},e=0n;for(let o of Object.keys(t)){let r=n[o];typeof r=="number"&&(e+=b(r,t[o],o))}return e}function Y(n,t){let e=["years","months","weeks","days","hours","minutes","seconds","milliseconds","microseconds","nanoseconds"],o={};for(let r of e)o[r]=(n[r]??0)+(t[r]??0);return o}function kn(n,t){return Y(n,Z(t))}function Z(n){let t={};for(let e of Object.keys(n))t[e]=-(n[e]??0);return t}export{P as a,J as b,Q as c,nn as d,z as e,rn as f,dn as g,mn as h,fn as i,hn as j,gn as k,yn as l,pn as m,Y as n,kn as o};
|
|
7
|
+
//# sourceMappingURL=chunk-2JHEV7ZF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rounding.ts","../src/formatDuration.ts","../src/duration.ts"],"sourcesContent":["// Rounding helpers. Pure functions over field bags,\n// same convention as arithmetic.ts/calendarUtils.ts.\n\nimport { asDateFieldView, type DateFieldView } from './calendarUtils.js';\nimport { InvalidDurationError } from './errors.js';\n\ntype RoundingUnit = 'day' | 'hour' | 'minute' | 'second' | 'millisecond';\ntype RoundingMode = 'nearest' | 'floor' | 'ceil' | 'trunc';\n\ninterface DateTimeFieldView extends DateFieldView {\n hour?: number;\n minute?: number;\n second?: number;\n millisecond?: number;\n}\n\n// Ms-per-unit table. Used by round() to convert a field bag to ms,\n// apply the rounding mode, and convert back. Mirrors how add() in\n// arithmetic.ts handles sub-day arithmetic.\nconst MS_PER_UNIT: Record<RoundingUnit, number> = {\n day: 86_400_000,\n hour: 3_600_000,\n minute: 60_000,\n second: 1_000,\n millisecond: 1,\n};\n\nfunction applyMode(ms: number, mode: RoundingMode, increment: number): number {\n // All four modes operate on absolute value then re-apply the sign,\n // so negative values round symmetrically (floor of -1.5 → -2, not -1).\n // Matches Math.round semantics for 'nearest' and Temporal's rounding\n // for the other three.\n const sign = ms < 0 ? -1 : 1;\n const abs = Math.abs(ms);\n const stepped = abs / increment;\n let rounded: number;\n switch (mode) {\n case 'nearest': rounded = Math.round(stepped); break;\n case 'floor': rounded = Math.floor(stepped); break;\n case 'ceil': rounded = Math.ceil(stepped); break;\n case 'trunc': rounded = Math.trunc(stepped); break;\n }\n return sign * rounded * increment;\n}\n\nfunction toMs(v: DateTimeFieldView): number {\n // Returns ms-since-(Howard Hinnant epoch). Used internally — the\n // absolute value is meaningful only relative to the same epoch used\n // by fromMs.\n const y = v.year!, m = v.month!, d = v.day!;\n const y2 = m <= 2 ? y - 1 : y;\n const era = Math.floor((y2 >= 0 ? y2 : y2 - 399) / 400);\n const yoe = y2 - era * 400;\n const m2 = m > 2 ? m - 3 : m + 9;\n const doy = Math.floor((153 * m2 + 2) / 5) + d - 1;\n const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;\n const days = era * 146097 + doe - 719468;\n return days * MS_PER_UNIT.day\n + (v.hour ?? 0) * MS_PER_UNIT.hour\n + (v.minute ?? 0) * MS_PER_UNIT.minute\n + (v.second ?? 0) * MS_PER_UNIT.second\n + (v.millisecond ?? 0);\n}\n\nfunction fromMs(ms: number, base: DateTimeFieldView): DateTimeFieldView {\n // Convert absolute ms (on the same epoch as toMs) back to a field bag.\n // Walks the Howard Hinnant days_from_civil inverse inline.\n const MS_PER_DAY = MS_PER_UNIT.day;\n const totalDays = Math.floor(ms / MS_PER_DAY);\n let withinDay = ms - totalDays * MS_PER_DAY; // ms since midnight\n // Defensive floating-point guard: given totalDays = Math.floor(ms /\n // MS_PER_DAY), withinDay = ms - totalDays * MS_PER_DAY is\n // mathematically guaranteed non-negative (that's what Math.floor\n // division gives you), verified against extreme values including\n // Number.MIN_SAFE_INTEGER and sub-ms fractional noise near day\n // boundaries. Kept in case a future change to how ms is computed\n // upstream breaks that guarantee.\n /* c8 ignore next */\n if (withinDay < 0) withinDay += MS_PER_DAY;\n const hour = Math.floor(withinDay / MS_PER_UNIT.hour);\n const minute = Math.floor((withinDay % MS_PER_UNIT.hour) / MS_PER_UNIT.minute);\n const second = Math.floor((withinDay % MS_PER_UNIT.minute) / MS_PER_UNIT.second);\n const millisecond = withinDay % MS_PER_UNIT.second;\n // Convert totalDays back to year/month/day via Howard Hinnant's\n // civil_from_days. Same algorithm as arithmetic.ts's shiftDays.\n const z = totalDays + 719468;\n const era = Math.floor((z >= 0 ? z : z - 146096) / 146097);\n const doe = z - era * 146097;\n const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365);\n const y2 = yoe + era * 400;\n const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));\n const mp = Math.floor((5 * doy + 2) / 153);\n const d = doy - Math.floor((153 * mp + 2) / 5) + 1;\n const m = mp < 10 ? mp + 3 : mp - 9;\n const year = m <= 2 ? y2 + 1 : y2;\n return { ...base, year, month: m, day: d, hour, minute, second, millisecond };\n}\n\nfunction shiftDays(v: DateTimeFieldView, days: number): DateTimeFieldView {\n const y = v.year!, m = v.month!, d = v.day!;\n const y2 = m <= 2 ? y - 1 : y;\n const era = Math.floor((y2 >= 0 ? y2 : y2 - 399) / 400);\n const yoe = y2 - era * 400;\n const m2 = m > 2 ? m - 3 : m + 9;\n const doy = Math.floor((153 * m2 + 2) / 5) + d - 1;\n const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;\n const totalDays = era * 146097 + doe - 719468 + days;\n const z = totalDays + 719468;\n const era2 = Math.floor((z >= 0 ? z : z - 146096) / 146097);\n const doe2 = z - era2 * 146097;\n const yoe2 = Math.floor((doe2 - Math.floor(doe2 / 1460) + Math.floor(doe2 / 36524) - Math.floor(doe2 / 146096)) / 365);\n const y2out = yoe2 + era2 * 400;\n const doy2 = doe2 - (365 * yoe2 + Math.floor(yoe2 / 4) - Math.floor(yoe2 / 100));\n const mp = Math.floor((5 * doy2 + 2) / 153);\n const d2 = doy2 - Math.floor((153 * mp + 2) / 5) + 1;\n const m2out = mp < 10 ? mp + 3 : mp - 9;\n const yOut = m2out <= 2 ? y2out + 1 : y2out;\n return { ...v, year: yOut, month: m2out, day: d2 };\n}\n\nexport interface RoundOptions {\n unit: RoundingUnit;\n mode?: RoundingMode;\n roundingIncrement?: number;\n}\n\nexport function round(value: unknown, options: RoundOptions): DateTimeFieldView {\n const base = asDateFieldView(value) as DateTimeFieldView;\n const mode = options.mode ?? 'nearest';\n const increment = (options.roundingIncrement ?? 1) * MS_PER_UNIT[options.unit];\n if (increment <= 0) {\n throw new Error(`temporal-fmt: round() requires a positive roundingIncrement (got ${options.roundingIncrement}).`);\n }\n const ms = toMs(base);\n const rounded = applyMode(ms, mode, increment);\n return fromMs(rounded, base);\n}\n\nexport function floor(value: unknown, unit: RoundingUnit, roundingIncrement: number = 1): DateTimeFieldView {\n return round(value, { unit, mode: 'floor', roundingIncrement });\n}\n\nexport function ceil(value: unknown, unit: RoundingUnit, roundingIncrement: number = 1): DateTimeFieldView {\n return round(value, { unit, mode: 'ceil', roundingIncrement });\n}\n\nexport function truncate(value: unknown, unit: RoundingUnit, roundingIncrement: number = 1): DateTimeFieldView {\n return round(value, { unit, mode: 'trunc', roundingIncrement });\n}\n\n// Duration rounding. Takes a duration field bag and rounds it to the\n// requested unit. Operates on the duration's own fields (years/months\n// for calendar-bound units, days/hours/minutes/seconds/ms/µs/ns for\n// absolute units). Mirrors Temporal.Duration.prototype.round's surface\n// — but operates on plain field bags rather than real Temporal.Duration\n// instances, same convention as the rest of this module set.\nexport interface DurationFields {\n years?: number;\n months?: number;\n weeks?: number;\n days?: number;\n hours?: number;\n minutes?: number;\n seconds?: number;\n milliseconds?: number;\n microseconds?: number;\n nanoseconds?: number;\n}\n\n// Converts one absolute-unit field to nanoseconds, exactly. Integer\n// values keep the exact BigInt path (arbitrary magnitude). Fractional\n// values — which parseISODuration legitimately produces (\"P1.5D\") —\n// scale in floating point and are accepted only when the product is a\n// safe integer, so P1.5D balances to exactly 1 day + 12 hours. A\n// fractional value whose product can't be represented exactly (large\n// fractional day counts) throws the typed InvalidDurationError instead\n// of the opaque \"TypeError: Cannot convert 1.5 to a BigInt\" the old\n// BigInt(v) call produced. Shared by roundDuration (here) and the\n// duration.ts arithmetic helpers — duration.ts already imports from\n// this module, so exporting keeps the dependency direction acyclic.\nexport function fieldToNs(v: number, nsPer: bigint, field: string): bigint {\n if (Number.isInteger(v)) return BigInt(v) * nsPer;\n const scaled = v * Number(nsPer);\n if (Number.isSafeInteger(scaled)) return BigInt(scaled);\n throw new InvalidDurationError({\n reason: `field \"${field}\" (${v}) cannot be converted to nanoseconds exactly — the value is too large for a fractional unit; use smaller units`,\n });\n}\n\ntype DurationUnit = 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds' | 'microseconds' | 'nanoseconds';\n\nconst DURATION_UNIT_TO_NS: Record<DurationUnit, bigint> = {\n years: 0n, // calendar-bound, can't convert without relativeTo\n months: 0n, // same\n weeks: 0n, // same\n days: 86_400n * 1_000_000_000n,\n hours: 3_600n * 1_000_000_000n,\n minutes: 60n * 1_000_000_000n,\n seconds: 1_000_000_000n,\n milliseconds: 1_000_000n,\n microseconds: 1_000n,\n nanoseconds: 1n,\n};\n\n// Absolute units (days and below) can be rounded without a relativeTo\n// because their length is fixed in nanoseconds. Calendar-bound units\n// (years/months/weeks) can't — would need a Temporal.Duration round()\n// call with a relativeTo. This helper throws if the caller tries to\n// round to/from a calendar-bound unit, rather than silently producing\n// a wrong result.\nfunction isCalendarBound(unit: DurationUnit): boolean {\n return unit === 'years' || unit === 'months' || unit === 'weeks';\n}\n\nexport function roundDuration(duration: DurationFields, options: {\n unit: DurationUnit;\n mode?: RoundingMode;\n roundingIncrement?: number;\n}): DurationFields {\n const mode = options.mode ?? 'nearest';\n const increment = BigInt(options.roundingIncrement ?? 1);\n if (increment <= 0n) {\n throw new Error(`temporal-fmt: roundDuration() requires a positive roundingIncrement (got ${options.roundingIncrement}).`);\n }\n if (isCalendarBound(options.unit)) {\n throw new Error(\n `temporal-fmt: roundDuration() to \"${options.unit}\" requires a Temporal.Duration with a relativeTo — ` +\n `this helper operates on plain field bags without calendar context. Use Temporal.Duration.prototype.round() directly.`\n );\n }\n\n // Sum all absolute-unit contributions into total nanoseconds.\n let totalNs = 0n;\n for (const u of ['days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'] as DurationUnit[]) {\n const v = duration[u] ?? 0;\n totalNs += fieldToNs(v, DURATION_UNIT_TO_NS[u], u);\n }\n\n // Apply rounding on the absolute value, preserving sign (same as round()).\n const stepNs = increment * DURATION_UNIT_TO_NS[options.unit];\n const sign = totalNs < 0n ? -1n : 1n;\n const abs = totalNs < 0n ? -totalNs : totalNs;\n const stepped = abs / stepNs;\n const remainder = abs % stepNs;\n let rounded: bigint;\n switch (mode) {\n case 'nearest':\n // Banker's rounding would be the Temporal spec, but Math.round-style\n // half-up is what callers typically expect from a \"round\" function.\n // Going with half-up to match round() above and avoid surprising anyone\n // who reads the output.\n rounded = remainder * 2n >= stepNs ? stepped + 1n : stepped;\n break;\n case 'floor': rounded = stepped; break;\n case 'ceil': rounded = remainder > 0n ? stepped + 1n : stepped; break;\n case 'trunc': rounded = stepped; break;\n }\n const newTotalNs = sign * rounded * stepNs;\n\n // Distribute back into the largest unit ≤ the requested unit. This\n // produces a balanced duration (e.g. rounding 90s to minutes gives\n // 1m30s in the minute field, not 90s).\n const result: DurationFields = { ...duration };\n let remaining = newTotalNs;\n // Clear all absolute units, then re-populate from largest to smallest so\n // the balanced output has all its value concentrated at the target unit\n // and below.\n const unitsInOrder: DurationUnit[] = ['days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'];\n const startIdx = unitsInOrder.indexOf(options.unit);\n // Units finer than the rounding target are zeroed: all their value was\n // folded into `remaining` above, and whatever survives rounding now\n // lives at the target unit and above. (Units coarser than the target\n // are re-written by the distribution loop below — their value is\n // already inside `remaining`, so the spread copy is overwritten with\n // the same number.)\n for (let i = startIdx + 1; i < unitsInOrder.length; i++) {\n result[unitsInOrder[i]!] = 0;\n }\n // Re-populate from the largest absolute unit down to the target, so the\n // result has all its value concentrated at the target unit and above.\n for (let i = 0; i <= startIdx; i++) {\n const u = unitsInOrder[i]!;\n const unitNs = DURATION_UNIT_TO_NS[u];\n // Dead by construction: unitsInOrder only lists days/hours/minutes/\n // seconds/milliseconds/microseconds/nanoseconds, none of which are\n // 0n in DURATION_UNIT_TO_NS (only years/months/weeks are, and those\n // never appear in this array). Kept in case unitsInOrder grows to\n // include a calendar-bound unit later.\n /* c8 ignore next */\n if (unitNs === 0n) continue;\n const count = remaining / unitNs;\n remaining -= count * unitNs;\n // Assign unconditionally (zero counts write 0): the units at and above\n // the target are fully re-derived from the rounded total, so any\n // original spread-copied value must be overwritten either way —\n // conditionally skipping zero counts would leave the caller's stale\n // value sitting in the field.\n result[u] = Number(count);\n }\n return result;\n}","import { type FormatOptions } from './tokens.js';\nimport { tokenize } from './tokenize.js';\nimport { MAX_FORMAT_LENGTH } from './constants.js';\nimport { canonicalCacheKey, normalizeLocaleTag } from './localeVocab.js';\nimport { InvalidLocaleError } from './errors.js';\n\n// A Temporal.Duration doesn't sit on a calendar — it has no year/month/day\n// position the way a PlainDate does — so the date/time token set in\n// tokens.ts doesn't apply. This is a separate token table for the same\n// reason parseRelative() lives in its own module: a different surface\n// deserves its own grammar, not a bolt-on to the existing one.\n//\n// Token design: each unit has three forms, in increasing verbosity.\n// `y` — numeric value, no unit text (\"2\")\n// `yy` — short unit suffix, plural-aware (\"2y\", or \"1y\" — see below)\n// `yyy` — full unit word, plural-aware (\"2 years\", \"1 year\")\n// Same for months (`o`/`oo`/`ooo` — `o` not `M` because the duration's\n// own field name in Temporal is \"months\" and the long-form output should\n// read \"month\"/\"months\", but the *token* letter is arbitrary; `o` is\n// chosen so it doesn't visually collide with `m` (minutes) or `M`\n// (months in date format) when a reader skims both tables side by side).\n//\n// Locale-aware rendering: when a `locale` is passed, the short/long\n// forms delegate to `Intl.NumberFormat` with `style: 'unit'`. That gives\n// us pluralization, native unit names, and the locale's preferred\n// spacing for free, matching the same \"delegate to Intl\" approach\n// `formatDistance` already uses. Without a `locale`, the original\n// English-only hardcoded table is used so existing default output stays\n// byte-identical — this is an additive locale option, not a rewrite of\n// the default rendering path.\n\ntype UnitKey = 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds';\n\ninterface UnitForms {\n // singular and plural suffixes for the long form (e.g. \"year\", \"years\")\n longSingular: string;\n longPlural: string;\n // singular and plural suffixes for the short form (e.g. \"yr\", \"yrs\")\n shortSingular: string;\n shortPlural: string;\n // the property on Temporal.Duration to read for this unit\n field: UnitKey;\n // Intl.NumberFormat unit identifier (used only on the locale-aware path).\n // Confirmed against the current Intl spec — including `millisecond`, which\n // is in fact supported by every engine we target (Node 20+, modern ICU).\n // The task brief flagged this as a possible gap; empirically it isn't.\n intlUnit: string;\n}\n\nconst DURATION_UNITS: Record<string, UnitForms> = {\n y: { longSingular: 'year', longPlural: 'years', shortSingular: 'yr', shortPlural: 'yrs', field: 'years', intlUnit: 'year' },\n o: { longSingular: 'month', longPlural: 'months', shortSingular: 'mo', shortPlural: 'mos', field: 'months', intlUnit: 'month' },\n w: { longSingular: 'week', longPlural: 'weeks', shortSingular: 'wk', shortPlural: 'wks', field: 'weeks', intlUnit: 'week' },\n d: { longSingular: 'day', longPlural: 'days', shortSingular: 'd', shortPlural: 'd', field: 'days', intlUnit: 'day' },\n h: { longSingular: 'hour', longPlural: 'hours', shortSingular: 'h', shortPlural: 'h', field: 'hours', intlUnit: 'hour' },\n m: { longSingular: 'minute', longPlural: 'minutes', shortSingular: 'm', shortPlural: 'm', field: 'minutes', intlUnit: 'minute' },\n s: { longSingular: 'second', longPlural: 'seconds', shortSingular: 's', shortPlural: 's', field: 'seconds', intlUnit: 'second' },\n S: { longSingular: 'millisecond', longPlural: 'milliseconds', shortSingular: 'ms', shortPlural: 'ms', field: 'milliseconds', intlUnit: 'millisecond' },\n};\n\n// Token strings this module recognizes, longest-first for the greedy\n// tokenizer. tokenize() in tokenize.ts already does longest-first via\n// SORTED_TOKEN_STRINGS, but it operates on the *date/time* TOKENS table —\n// not this one. Rather than teach tokenize() about two tables, this\n// module uses its own equivalent scan over DURATION_UNITS keys.\nconst DURATION_TOKEN_STRINGS = Object.keys(DURATION_UNITS)\n .flatMap((single) => [single + single + single, single + single, single])\n .sort((a, b) => b.length - a.length);\n\ntype Piece =\n | { kind: 'token'; value: string; unit: string; form: 'numeric' | 'short' | 'long' }\n | { kind: 'literal'; value: string };\n\n// Same greedy-match shape as tokenize() in tokenize.ts, but with this\n// module's own token set. Quoted-literal handling is identical so the\n// same documented escaping rules carry over: 'at' is a literal, '' is a\n// literal quote.\nfunction tokenizeDuration(format: string): Piece[] {\n const pieces: Piece[] = [];\n let i = 0;\n while (i < format.length) {\n const ch = format[i];\n if (ch === \"'\") {\n if (format[i + 1] === \"'\") {\n appendLiteral(pieces, \"'\");\n i += 2;\n continue;\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 if (!closed) {\n throw new Error(`temporal-fmt: unterminated quote in duration format string \"${format}\"`);\n }\n appendLiteral(pieces, literal);\n i = j;\n continue;\n }\n const match = DURATION_TOKEN_STRINGS.find((tok) => format.startsWith(tok, i));\n if (match) {\n // single letter = numeric, double = short, triple = long\n const unit = match[0]!;\n const form = match.length === 1 ? 'numeric' : match.length === 2 ? 'short' : 'long';\n pieces.push({ kind: 'token', value: match, unit, form });\n i += match.length;\n continue;\n }\n appendLiteral(pieces, ch);\n i += 1;\n }\n return pieces;\n}\n\nfunction appendLiteral(pieces: Piece[], value: string): void {\n const last = pieces[pieces.length - 1];\n if (last && last.kind === 'literal') {\n last.value += value;\n } else {\n pieces.push({ kind: 'literal', value });\n }\n}\n\n// Reads a unit value off the duration in a way that works for both real\n// Temporal.Duration objects and field bags ({ hours: 2, minutes: 30 }).\n// Temporal.Duration values are numbers, possibly negative for\n// backwards durations; a field bag the caller passes might leave fields\n// off entirely (undefined → treated as 0 for this unit).\nfunction readUnit(duration: Record<string, unknown>, field: UnitKey): number {\n const value = duration[field];\n if (value === undefined || value === null) return 0;\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n // Temporal.Duration values are always numbers, but a hand-built field\n // bag could end up with a string here. Coerce; throw on garbage rather\n // than silently formatting NaN.\n const coerced = Number(value);\n if (!Number.isFinite(coerced)) {\n throw new Error(\n `temporal-fmt: duration field \"${field}\" is not a finite number (got ${String(value)}).`\n );\n }\n return coerced;\n}\n\n// Intl.NumberFormat is expensive to construct, and a typical duration\n// render walks 2-4 units — so a tight loop on the same (locale, unit,\n// display) triple would otherwise rebuild the formatter for every token\n// of every row. Cache by canonical-locale + unit + display, same\n// eviction shape as the other locale-keyed caches in this library\n// (formatterCache in tokens.ts, rtfCache in formatDistance.ts).\nconst unitFormatterCache = new Map<string, Intl.NumberFormat>();\nconst MAX_UNIT_FORMATTER_CACHE_SIZE = 200;\n\nfunction getUnitFormatter(locale: string, intlUnit: string, unitDisplay: 'short' | 'long'): Intl.NumberFormat {\n const key = `${canonicalCacheKey(locale)}|${intlUnit}|${unitDisplay}`;\n const cached = unitFormatterCache.get(key);\n if (cached) return cached;\n if (unitFormatterCache.size >= MAX_UNIT_FORMATTER_CACHE_SIZE) {\n const oldestKey = unitFormatterCache.keys().next().value;\n if (oldestKey !== undefined) unitFormatterCache.delete(oldestKey);\n }\n let formatter: Intl.NumberFormat;\n try {\n formatter = new Intl.NumberFormat(normalizeLocaleTag(locale), {\n style: 'unit',\n unit: intlUnit,\n unitDisplay,\n });\n } catch (err) {\n // Malformed locale tags reach Intl as a bare RangeError; surface the\n // library's typed error instead. (Every failure mode of these\n // constructors with a string locale is a RangeError, so converting\n // unconditionally loses nothing — the original message is preserved\n // in `reason` either way.)\n throw new InvalidLocaleError({ actual: locale, reason: (err as Error).message });\n }\n unitFormatterCache.set(key, formatter);\n return formatter;\n}\n\nexport interface DurationFormatOptions extends FormatOptions {\n /**\n * When true, units whose value is zero are still emitted in the output\n * (e.g. \"0 hours, 30 minutes\" instead of \"30 minutes\"). Default is\n * false: zero-value units are omitted, matching how date-fns's\n * formatDuration works and how most callers rendering a duration to\n * a human would want it.\n */\n showZeroValues?: boolean;\n}\n\n/**\n * Format a Temporal.Duration (or a plain field bag { years, months, ...\n * }) using a duration-specific token string. Token grammar is documented\n * in the README under \"Duration formatting\" — it does NOT reuse the\n * date/time token table, since a duration has no calendar position.\n *\n * Zero-value units are omitted by default; pass { showZeroValues: true }\n * to force them to appear.\n *\n * Unit-name localization: without a `locale`, output is the original\n * English hardcoded singular/plural forms (byte-identical to previous\n * versions). With a `locale`, the short/long forms delegate to\n * `Intl.NumberFormat`'s `style: 'unit'` — same approach `formatDistance`\n * already uses for `Intl.RelativeTimeFormat`. Numeric-only tokens\n * (`y`, `o`, `w`, ...) are not affected by `locale`; they remain ASCII\n * digits, matching the rest of this library's \"numbers stay Western\"\n * convention.\n *\n * @example\n * formatDuration(Temporal.Duration.from({ years: 2, months: 1 }), 'yyy ooo')\n * // \"2 years 1 month\"\n * formatDuration({ hours: 2, minutes: 30 }, 'hhh mmm', { locale: 'fr-FR' })\n * // \"2 heures 30 minutes\"\n */\nexport function formatDuration(\n duration: Record<string, unknown>,\n formatStr: string,\n options: DurationFormatOptions = {},\n): string {\n return renderDurationPieces(duration, formatStr, options).map((p) => p.value).join('');\n}\n\n// One rendered piece of a duration format string: either literal text or\n// a token with its already-rendered value (empty string for a\n// zero-suppressed token, matching formatDurationToParts' documented\n// contract of emitting an empty token part rather than dropping it).\nexport interface RenderedDurationPiece {\n kind: 'literal' | 'token';\n value: string;\n token?: string;\n}\n\n// Shared renderer behind both formatDuration() (joins the pieces) and\n// formatDurationToParts() (returns them). Single source of truth, so the\n// joined string and the parts can never disagree — the old\n// formatDurationToParts re-derived part boundaries by searching the\n// rendered string with indexOf and mis-sliced whenever a literal's text\n// also occurred inside an earlier token's rendered value.\nexport function renderDurationPieces(\n duration: Record<string, unknown>,\n formatStr: string,\n options: DurationFormatOptions = {},\n): RenderedDurationPiece[] {\n if (formatStr.length > MAX_FORMAT_LENGTH) {\n throw new Error(\n `temporal-fmt: duration format string exceeds maximum length of ${MAX_FORMAT_LENGTH} characters ` +\n `(got ${formatStr.length}).`\n );\n }\n\n const showZeroes = options.showZeroValues === true;\n // Undefined locale → keep the original hardcoded English path so\n // existing default output is byte-identical. Any locale string passed\n // (including 'en-US') takes the Intl.NumberFormat path — so an\n // explicit `locale: 'en-US'` will produce Intl's spacing/word choices\n // rather than the hand-rolled English table. That's intentional: the\n // locale-aware path delegates to Intl, the default path doesn't.\n const locale = options.locale;\n const useIntl = locale !== undefined;\n\n const pieces = tokenizeDuration(formatStr);\n const out: RenderedDurationPiece[] = [];\n\n for (const piece of pieces) {\n if (piece.kind === 'literal') {\n out.push({ kind: 'literal', value: piece.value });\n continue;\n }\n\n const unit = DURATION_UNITS[piece.unit];\n // Dead by construction: piece.unit always comes from match[0] where\n // match is a member of DURATION_TOKEN_STRINGS, itself built\n // exhaustively from DURATION_UNITS' own keys — so this lookup can\n // never miss. Kept in case tokenizeDuration's token construction\n // ever diverges from DURATION_UNITS.\n /* c8 ignore start */\n if (!unit) {\n throw new Error(`temporal-fmt: unknown duration token \"${piece.value}\"`);\n }\n /* c8 ignore stop */\n\n const value = readUnit(duration, unit.field);\n if (value === 0 && !showZeroes) {\n // Omit zero-value units entirely. The token's contribution to the\n // output is empty — but this can leave a dangling separator literal\n // (e.g. \"2 hours, \" with nothing after). Callers who want clean\n // output should structure their format string to not put a\n // separator after a unit that might be zero, or use showZeroValues.\n out.push({ kind: 'token', value: '', token: piece.value });\n continue;\n }\n\n if (piece.form === 'numeric') {\n out.push({ kind: 'token', value: String(value), token: piece.value });\n continue;\n }\n\n if (useIntl) {\n // Intl.NumberFormat handles pluralization, native unit names, and\n // locale-appropriate spacing all in one call — no point replicating\n // that here. We pass the value as-is so negative durations\n // (`-1 hour`) round-trip the sign the way Intl expects.\n const unitDisplay = piece.form === 'short' ? 'short' : 'long';\n const formatter = getUnitFormatter(locale!, unit.intlUnit, unitDisplay);\n out.push({ kind: 'token', value: formatter.format(value), token: piece.value });\n continue;\n }\n\n // Default-path English. Plural rule based on absolute value matches\n // what Intl does for unit style on en-US — only |value| === 1 is\n // singular, including for negatives like \"-1 hour\" (which is what\n // Intl produces for en-US too).\n if (piece.form === 'short') {\n out.push({ kind: 'token', value: value + (value === 1 || value === -1 ? unit.shortSingular : unit.shortPlural), token: piece.value });\n } else {\n out.push({ kind: 'token', value: value + ' ' + (value === 1 || value === -1 ? unit.longSingular : unit.longPlural), token: piece.value });\n }\n }\n\n return out;\n}","// Duration extensions. The existing formatDuration()\n// in formatDuration.ts stays as-is; this module adds the rest of the\n// duration surface: parseDuration, parseISODuration, formatISODuration,\n// formatDurationToParts, balanceDuration, roundDuration (re-exported\n// from rounding.ts), totalDuration, compareDuration, addDuration,\n// subtractDuration.\n//\n// All operate on plain field bags ({ years, months, weeks, days, hours,\n// minutes, seconds, milliseconds, microseconds, nanoseconds }) rather\n// than Temporal.Duration instances — same convention as the rest of\n// this module set, so callers can use these without committing to a\n// Temporal implementation. addDuration/subtractDuration produce a new\n// field bag; totalDuration produces a number; compareDuration produces\n// -1/0/1.\n\nimport { roundDuration, fieldToNs, type DurationFields } from './rounding.js';\nimport { formatDuration, renderDurationPieces } from './formatDuration.js';\nimport { InvalidDurationError, FormatSyntaxError } from './errors.js';\n\n// Re-export roundDuration here so callers can import everything duration-\n// related from one place.\nexport { roundDuration };\nexport type { DurationFields };\n\n// Mirrors formatToParts in format.ts. Returns the same piece list\n// formatDuration() would consume, but split into parts instead of\n// joined. Useful for callers building custom UIs (one DOM node per\n// unit) or doing their own unit suppression logic.\n\nimport type { Piece } from './tokenize.js';\n\nexport interface DurationPart {\n type: 'literal' | 'token';\n value: string;\n token?: string;\n}\n\n// Delegates to formatDuration()'s own piece renderer, so the joined\n// string and the parts can never disagree. (This used to re-tokenize the\n// format string and re-derive part boundaries by searching the rendered\n// string with indexOf — which mis-sliced whenever a literal's text also\n// occurred inside an earlier token's rendered value, e.g. a \"s\"\n// separator against \"5 seconds\".) Zero-suppressed tokens emit an empty\n// token part and their neighboring literals are kept, matching the\n// documented formatDurationToParts contract.\nexport function formatDurationToParts(\n duration: Record<string, unknown>,\n formatStr: string,\n options: Parameters<typeof formatDuration>[2] = {},\n): DurationPart[] {\n return renderDurationPieces(duration, formatStr, options).map((p) =>\n p.kind === 'token' ? { type: 'token' as const, value: p.value, token: p.token } : { type: 'literal' as const, value: p.value },\n );\n}\n\n// ISO 8601 duration format: P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S\n// e.g. \"P3Y6M4DT12H30M5S\", \"PT1H30M\", \"P1W\", \"P0D\".\n// Parsed into a DurationFields bag.\nexport function parseISODuration(input: string): DurationFields {\n // Loose grammar: P [years Y] [months M] [weeks W] [days D] [T [hours H] [minutes M] [seconds S]]\n // Weeks is an ISO 8601-2 extension; widely supported.\n const re = /^P(?:(?:(\\d+(?:\\.\\d+)?)Y)?(?:(\\d+(?:\\.\\d+)?)M)?(?:(\\d+(?:\\.\\d+)?)W)?(?:(\\d+(?:\\.\\d+)?)D)?)?(?:T(?:(\\d+(?:\\.\\d+)?)H)?(?:(\\d+(?:\\.\\d+)?)M)?(?:(\\d+(?:\\.\\d+)?)S)?)?$/;\n const m = re.exec(input);\n if (!m) {\n throw new InvalidDurationError({ input, reason: 'does not match ISO 8601 duration grammar (P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S)' });\n }\n // All-zero / empty: P0D is valid ISO for zero duration.\n if (m.slice(1).every((g) => g === undefined)) {\n throw new InvalidDurationError({ input, reason: 'duration has no fields (use \"P0D\" for zero duration)' });\n }\n const toNum = (s: string | undefined): number => s === undefined ? 0 : Number(s);\n return {\n years: toNum(m[1]),\n months: toNum(m[2]),\n weeks: toNum(m[3]),\n days: toNum(m[4]),\n hours: toNum(m[5]),\n minutes: toNum(m[6]),\n seconds: toNum(m[7]),\n };\n}\n\nexport function formatISODuration(duration: DurationFields): string {\n const parts: string[] = ['P'];\n const years = duration.years ?? 0;\n const months = duration.months ?? 0;\n const weeks = duration.weeks ?? 0;\n const days = duration.days ?? 0;\n const hours = duration.hours ?? 0;\n const minutes = duration.minutes ?? 0;\n const seconds = duration.seconds ?? 0;\n if (years) parts.push(`${years}Y`);\n if (months) parts.push(`${months}M`);\n if (weeks) parts.push(`${weeks}W`);\n if (days) parts.push(`${days}D`);\n if (hours || minutes || seconds) {\n parts.push('T');\n if (hours) parts.push(`${hours}H`);\n if (minutes) parts.push(`${minutes}M`);\n if (seconds) parts.push(`${seconds}S`);\n }\n // parts is just ['P'] when every field is zero -- ISO has no empty\n // duration, so fall back to P0D.\n if (parts.length === 1) parts.push('0D');\n return parts.join('');\n}\n\n// Minimal tokenizer for the duration format string. Reuses the same\n// quoted-literal and greedy-match logic as tokenize.ts.\nfunction tokenizeDurationFormat(formatStr: string): Array<Piece & { unit?: string }> {\n const DURATION_TOKEN_STRINGS = ['y', 'o', 'w', 'd', 'h', 'm', 's', 'S']\n .flatMap((single) => [single + single + single, single + single, single])\n .sort((a, b) => b.length - a.length);\n const pieces: Array<Piece & { unit?: string }> = [];\n let i = 0;\n while (i < formatStr.length) {\n const ch = formatStr[i];\n if (ch === \"'\") {\n if (formatStr[i + 1] === \"'\") {\n appendLiteral(pieces, \"'\");\n i += 2;\n continue;\n }\n let j = i + 1;\n let literal = '';\n let closed = false;\n while (j < formatStr.length) {\n if (formatStr[j] === \"'\") {\n if (formatStr[j + 1] === \"'\") {\n literal += \"'\";\n j += 2;\n continue;\n }\n closed = true;\n j += 1;\n break;\n }\n literal += formatStr[j]!;\n j += 1;\n }\n if (!closed) throw new FormatSyntaxError({ format: formatStr, reason: 'unterminated quote' });\n appendLiteral(pieces, literal);\n i = j;\n continue;\n }\n const match = DURATION_TOKEN_STRINGS.find((tok) => formatStr.startsWith(tok, i));\n if (match) {\n pieces.push({ kind: 'token', value: match, unit: match[0] });\n i += match.length;\n continue;\n }\n appendLiteral(pieces, ch);\n i += 1;\n }\n return pieces;\n}\n\nfunction appendLiteral(pieces: Array<Piece & { unit?: string }>, value: string): void {\n const last = pieces[pieces.length - 1];\n if (last && last.kind === 'literal') {\n last.value += value;\n } else {\n pieces.push({ kind: 'literal', value });\n }\n}\n\nfunction findNextLiteralStart(\n tokens: Array<Piece & { unit?: string }>,\n _currentTok: Piece & { unit?: string },\n full: string,\n pos: number,\n): number {\n // Find the position in `full` where the next literal after the\n // current token starts. For now, just return the end of `full` —\n // formatDurationToParts is best-effort for the common case where\n // tokens are separated by literals, and degrades gracefully when\n // they aren't.\n // A more precise implementation would walk the tokens array from\n // the current position and use the next literal piece's text to\n // find the boundary in `full`. Doing it correctly requires handling\n // zero-value unit suppression, which is why this helper exists.\n // For now, we use the next literal piece's value as a search anchor.\n const idx = tokens.indexOf(_currentTok);\n for (let j = idx + 1; j < tokens.length; j++) {\n const t = tokens[j]!;\n if (t.kind === 'literal') {\n const found = full.indexOf(t.value, pos);\n if (found >= 0) return found;\n }\n }\n return full.length;\n}\n\n// ISO 8601 duration format: P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S\n// Parses a duration-format string (the tokenized format formatDuration\n// accepts) into a DurationFields bag. Inverse of formatDuration().\nexport function parseDuration(input: string, formatStr: string, options: { locale?: string } = {}): DurationFields {\n // Build a capturing regex from the format string. Each token\n // captures a number; literals are matched literally.\n const tokens = tokenizeDurationFormat(formatStr);\n const fields: DurationFields = {};\n const groupSpecs: Array<{ unit: keyof DurationFields; form: 'numeric' | 'short' | 'long' }> = [];\n let regex = '';\n for (const tok of tokens) {\n if (tok.kind === 'literal') {\n regex += tok.value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n } else {\n const unit = tok.unit!;\n const form = tok.value.length === 1 ? 'numeric' : tok.value.length === 2 ? 'short' : 'long';\n // Map token letter to DurationFields key.\n /* c8 ignore start @preserve -- the final `: null` branch, and the\n `if (key === null)` throw it feeds, are both unreachable: tok.unit\n is always one of the 8 letters in DURATION_TOKEN_STRINGS (the same\n source set this ternary checks against), so `null` can't be\n produced by any token the tokenizer emits. Kept as a guard against\n a future letter being added to one set without the other. */\n const key: keyof DurationFields | null = (\n unit === 'y' ? 'years'\n : unit === 'o' ? 'months'\n : unit === 'w' ? 'weeks'\n : unit === 'd' ? 'days'\n : unit === 'h' ? 'hours'\n : unit === 'm' ? 'minutes'\n : unit === 's' ? 'seconds'\n : unit === 'S' ? 'milliseconds'\n : null\n );\n if (key === null) {\n throw new FormatSyntaxError({ format: formatStr, token: tok.value, reason: 'unknown duration token' });\n }\n /* c8 ignore stop @preserve */\n // Numeric form: capture digits (and optional sign).\n // Short/long form: capture digits followed by optional unit text.\n // We'll handle unit-text stripping after the match.\n if (form === 'numeric') {\n regex += '(-?\\\\d+)';\n } else {\n // Short form like \"2y\" or \"2yrs\"; long form like \"2 years\" or \"2 year\".\n // Capture the number; unit text is matched but not captured.\n regex += '(\\\\d+)\\\\s*\\\\w*';\n }\n groupSpecs.push({ unit: key, form });\n }\n }\n const re = new RegExp(`^${regex}$`, 'u');\n const m = re.exec(input);\n if (!m) {\n throw new FormatSyntaxError({ input, format: formatStr, reason: 'does not match duration format' });\n }\n groupSpecs.forEach((spec, i) => {\n const raw = m[i + 1]!;\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n throw new InvalidDurationError({ input, reason: `token for ${spec.unit} matched \"${raw}\" but isn't a finite number` });\n }\n fields[spec.unit] = value;\n });\n return fields;\n}\n\n// Balances a duration's absolute fields (days/hours/minutes/seconds/ms/µs/ns)\n// so each carries its natural range and excess carries into the next\n// larger unit. Doesn't touch calendar-bound fields (years/months/weeks)\n// since those need a relativeTo to balance correctly.\nexport function balanceDuration(duration: DurationFields): DurationFields {\n const NS_PER: Array<[keyof DurationFields, bigint]> = [\n ['days', 86_400n * 1_000_000_000n],\n ['hours', 3_600n * 1_000_000_000n],\n ['minutes', 60n * 1_000_000_000n],\n ['seconds', 1_000_000_000n],\n ['milliseconds', 1_000_000n],\n ['microseconds', 1_000n],\n ['nanoseconds', 1n],\n ];\n // Sum everything from days down to ns.\n let totalNs = 0n;\n for (const [k, nsPer] of NS_PER) {\n const v = duration[k];\n if (typeof v === 'number') totalNs += fieldToNs(v, nsPer, k);\n }\n // Re-distribute.\n const result: DurationFields = { ...duration };\n let remaining = totalNs;\n for (const [k, nsPer] of NS_PER) {\n const count = remaining / nsPer;\n remaining -= count * nsPer;\n result[k] = Number(count);\n }\n return result;\n}\n\n// Sums all absolute fields into a single number expressed in the\n// requested unit. Throws for calendar-bound target units (years/months/\n// weeks) since those need a relativeTo.\nexport function totalDuration(duration: DurationFields, unit: 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds' | 'microseconds' | 'nanoseconds'): number {\n const NS_PER: Record<string, bigint> = {\n days: 86_400n * 1_000_000_000n,\n hours: 3_600n * 1_000_000_000n,\n minutes: 60n * 1_000_000_000n,\n seconds: 1_000_000_000n,\n milliseconds: 1_000_000n,\n microseconds: 1_000n,\n nanoseconds: 1n,\n };\n let totalNs = 0n;\n for (const k of Object.keys(NS_PER) as Array<keyof typeof NS_PER>) {\n const v = duration[k as keyof DurationFields];\n if (typeof v === 'number') totalNs += fieldToNs(v, NS_PER[k]!, k);\n }\n const divisor = NS_PER[unit];\n if (!divisor) {\n throw new InvalidDurationError({ reason: `totalDuration() does not support unit \"${unit}\" (calendar-bound units need a relativeTo)` });\n }\n return Number(totalNs) / Number(divisor);\n}\n\n// Compares two durations by their total absolute length. Returns -1/0/1.\nexport function compareDuration(a: DurationFields, b: DurationFields): number {\n const aNs = totalDurationNs(a);\n const bNs = totalDurationNs(b);\n if (aNs < bNs) return -1;\n if (aNs > bNs) return 1;\n return 0;\n}\n\nfunction totalDurationNs(d: DurationFields): bigint {\n const NS_PER: Record<string, bigint> = {\n days: 86_400n * 1_000_000_000n,\n hours: 3_600n * 1_000_000_000n,\n minutes: 60n * 1_000_000_000n,\n seconds: 1_000_000_000n,\n milliseconds: 1_000_000n,\n microseconds: 1_000n,\n nanoseconds: 1n,\n };\n let total = 0n;\n for (const k of Object.keys(NS_PER)) {\n const v = d[k as keyof DurationFields];\n if (typeof v === 'number') total += fieldToNs(v, NS_PER[k]!, k);\n }\n return total;\n}\n\n// Adds two durations field-by-field. Doesn't balance — call\n// balanceDuration() afterwards if you want a balanced result.\nexport function addDuration(a: DurationFields, b: DurationFields): DurationFields {\n const keys: Array<keyof DurationFields> = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'];\n const result: DurationFields = {};\n for (const k of keys) {\n result[k] = (a[k] ?? 0) + (b[k] ?? 0);\n }\n return result;\n}\n\nexport function subtractDuration(a: DurationFields, b: DurationFields): DurationFields {\n return addDuration(a, negateDuration(b));\n}\n\nfunction negateDuration(d: DurationFields): DurationFields {\n const result: DurationFields = {};\n for (const k of Object.keys(d) as Array<keyof DurationFields>) {\n result[k] = -(d[k] ?? 0);\n }\n return result;\n}\n"],"mappings":"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","names":["MS_PER_UNIT","applyMode","ms","mode","increment","sign","stepped","rounded","toMs","v","y","m","d","y2","era","yoe","m2","doy","doe","fromMs","base","MS_PER_DAY","totalDays","withinDay","hour","minute","second","millisecond","z","mp","year","round","value","options","base","asDateFieldView","mode","increment","MS_PER_UNIT","ms","toMs","rounded","applyMode","fromMs","floor","unit","roundingIncrement","ceil","truncate","fieldToNs","v","nsPer","field","scaled","InvalidDurationError","DURATION_UNIT_TO_NS","isCalendarBound","roundDuration","duration","totalNs","u","stepNs","sign","abs","stepped","remainder","newTotalNs","result","remaining","unitsInOrder","startIdx","i","unitNs","count","DURATION_UNITS","DURATION_TOKEN_STRINGS","single","a","b","tokenizeDuration","format","pieces","i","ch","appendLiteral","j","literal","closed","match","tok","unit","form","value","last","readUnit","duration","field","coerced","unitFormatterCache","MAX_UNIT_FORMATTER_CACHE_SIZE","getUnitFormatter","locale","intlUnit","unitDisplay","key","canonicalCacheKey","cached","oldestKey","formatter","normalizeLocaleTag","err","InvalidLocaleError","formatDuration","formatStr","options","renderDurationPieces","p","showZeroes","useIntl","out","piece","formatDurationToParts","duration","formatStr","options","renderDurationPieces","p","parseISODuration","input","m","InvalidDurationError","g","toNum","s","formatISODuration","parts","years","months","weeks","days","hours","minutes","seconds","tokenizeDurationFormat","DURATION_TOKEN_STRINGS","single","a","b","pieces","i","ch","appendLiteral","j","literal","closed","FormatSyntaxError","match","tok","value","last","parseDuration","input","formatStr","options","tokens","tokenizeDurationFormat","fields","groupSpecs","regex","tok","unit","form","key","FormatSyntaxError","m","spec","i","raw","value","InvalidDurationError","balanceDuration","duration","NS_PER","totalNs","k","nsPer","v","fieldToNs","result","remaining","count","totalDuration","divisor","compareDuration","a","b","aNs","totalDurationNs","bNs","d","total","addDuration","keys","subtractDuration","negateDuration"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import{a as ue,f as J}from"./chunk-NPKJ7QFK.js";import{a as F}from"./chunk-GMGZZG6I.js";import{c as I}from"./chunk-3MZLTVP3.js";import{a as oe,b as l,c as V,d as x,e as M,g as z,h as j,j as q,m as ae,n as ie,q as B,r as me,t as A}from"./chunk-5U5WJ465.js";function ye(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function X(e,n=!1){let t=e.map(ye);return n?`(?:${t.map(be).join("|")})`:`(?:${t.join("|")})`}function be(e){return e.replace(/[a-zA-Z]/g,n=>`[${n.toLowerCase()}${n.toUpperCase()}]`)}var xe="(?:UTC|[+-]\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,9})?)?|[A-Za-z_]+(?:[+-]\\d{1,2})?(?:\\/[A-Za-z0-9_+-]+)*)",ke={X:"(?:Z|[+-]\\d{2}(?:\\d{2})?)",XX:"(?:Z|[+-]\\d{4})",XXX:"(?:Z|[+-]\\d{2}:\\d{2})",x:"[+-]\\d{2}(?:\\d{2})?",xx:"[+-]\\d{4}",xxx:"[+-]\\d{2}:\\d{2}"};function $e(){return xe}var Z;function Ee(){return Z||(Z=new Set(Intl.supportedValuesOf("timeZone")),Z.add("UTC")),Z}var Ne=/^[+-]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/;function W(e){if(Ne.test(e)||Ee().has(e))return!0;try{return I().ZonedDateTime.from({year:2026,month:1,day:1,hour:0,minute:0,second:0,timeZone:e}),!0}catch{return!1}}var Pe={yy:"\\d{2}",MM:"(?:0[1-9]|1[0-2])",M:"(?:1[0-2]|[1-9])",dd:"(?:0[1-9]|[12]\\d|3[01])",d:"(?:[12]\\d|3[01]|[1-9])",HH:"(?:[01]\\d|2[0-3])",H:"(?:1\\d|2[0-3]|[0-9])",hh:"(?:0[1-9]|1[0-2])",h:"(?:1[0-2]|[1-9])",mm:"(?:[0-5]\\d)",m:"(?:[1-5]\\d|[0-9])",ss:"(?:[0-5]\\d)",s:"(?:[1-5]\\d|[0-9])",SSSSSSSSS:"\\d{9}",SSSSSSSS:"\\d{8}",SSSSSSS:"\\d{7}",SSSSSS:"\\d{6}",SSSSS:"\\d{5}",SSSS:"\\d{4}",SSS:"\\d{3}",SS:"\\d{2}",S:"\\d",Q:"[1-4]"},Me="Q[1-4]",Te=new Set(["do","ww","RRRR","D","DD","DDD","LLLL","LLL","cccc","ccc","GGGG","G","zzzz","z"]),ve="-?\\d{4}",De="-?\\d{4,}",Oe="-?\\d+",ee=new Set(["yyyy","yy","y","MM","M","dd","d","HH","H","hh","h","mm","m","ss","s","SSSSSSSSS","SSSSSSSS","SSSSSSS","SSSSSS","SSSSS","SSSS","SSS","SS","S"]),de=new Set(["y"]);function L(e,n,t){if(e==="yyyy")return t!==void 0&&ee.has(t)?ve:De;if(e==="y")return Oe;let r=Pe[e];if(r)return r;if(e==="QQQ")return Me;if(Te.has(e))throw new V({token:e,message:`temporal-fmt: token "${e}" 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").`});let s=A(n);switch(e){case"MMMM":return X(s.monthLong);case"MMM":return X(s.monthShort);case"EEEE":return X(s.weekdayLong);case"EEE":return X(s.weekdayShort);case"a":return X(s.dayPeriod,!0);case"zzz":return $e();case"X":case"XX":case"XXX":case"x":case"xx":case"xxx":return ke[e];default:throw new V({token:e,message:`temporal-fmt: unknown token "${e}"`})}}var ce=new Set(["M","d","H","h","m","s"]),ze={M:[{digits:1,min:1,max:9},{digits:2,min:10,max:12}],d:[{digits:1,min:1,max:9},{digits:2,min:10,max:31}],H:[{digits:1,min:0,max:9},{digits:2,min:10,max:23}],h:[{digits:1,min:1,max:9},{digits:2,min:10,max:12}],m:[{digits:1,min:0,max:9},{digits:2,min:10,max:59}],s:[{digits:1,min:0,max:9},{digits:2,min:10,max:59}]};function ne(e,n){let t=new Map;function r(s,o){let i=`${s}:${o}`,u=t.get(i);if(u)return u;if(s===n.length){let a=o===e.length?[[]]:[];return t.set(i,a),a}let S=n[s],h=ze[S];/* c8 ignore start @preserve -- defensive guard, not reachable through
|
|
2
|
+
the public API. enumerateValidSplits's only caller (parse.ts, both
|
|
3
|
+
call sites) passes run.tokens straight from
|
|
4
|
+
pattern.ambiguousRuns, which parsePattern.ts only ever populates
|
|
5
|
+
with tokens already checked against UNPADDED_NUMERIC_TOKENS — the
|
|
6
|
+
exact same key set as UNPADDED_NUMERIC_RANGES. There's no path
|
|
7
|
+
where a token reaches here without having already passed that
|
|
8
|
+
check. */if(!h)throw new Error(`temporal-fmt: internal error \u2014 "${S}" is not an unpadded numeric token`);/* c8 ignore stop @preserve */let m=[];for(let{digits:a,min:g,max:f}of h){if(o+a>e.length)continue;let p=e.slice(o,o+a);if(a===2&&p[0]==="0")continue;let y=Number(p);if(!(y<g||y>f)){for(let D of r(s+1,o+a))if(m.push([y,...D]),m.length===2)break;if(m.length===2)break}}return t.set(i,m),m}return r(0,0)}function Ae(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var ge=12;function Xe(e){return Math.ceil(Math.log2(Math.max(e,1)))}function H(e){return e===void 0?!1:e.kind==="literal"?/^[0-9]/.test(e.value):ee.has(e.value)}function fe(e,n){let t=[],r=[],s="",o=0,i=0,u={names:[],tokens:[]},S=h=>{if(u.tokens.length===1){let m=u.names[0],a=u.tokens[0];s+=`(?<${m}>${L(a,n)})`,H(h)&&(i+=1)}else if(u.tokens.length>=2){let m=`r${o++}`,a=u.tokens.length;s+=`(?<${m}>\\d{${a},${a*2}})`,r.push({groupName:m,groupNames:u.names,tokens:u.tokens}),H(h)&&(i+=Xe(a+1))}u={names:[],tokens:[]}};for(let[h,m]of e.entries()){if(m.kind==="literal"){S(m),s+=Ae(m.value);continue}if(ce.has(m.value)){let p=`g${o++}`;t.push({name:p,token:m.value}),u.names.push(p),u.tokens.push(m.value);continue}S(m);let a=`g${o++}`;t.push({name:a,token:m.value});let g=e[h+1],f=g?.kind==="token"?g.value:void 0;if(de.has(m.value)&&H(g))throw new l({reason:`token "${m.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 "${m.value}" ends and the next field begins. Add a non-digit separator (e.g. "-" or " ") after it.`});m.value==="yyyy"&&f===void 0&&H(g)?s+=`(?<${a}>${L(m.value,n,"M")})`:s+=`(?<${a}>${L(m.value,n,f)})`}if(S(void 0),i>ge)throw new l({reason:`format string has too many variable-width numeric tokens glued to digit-consuming neighbors (ambiguity score ${i} > ${ge}). 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).`});return{regex:new RegExp(`^(?:${s})$`,"ud"),groups:t,ambiguousRuns:r}}var v=new Map,_e=500;ie(()=>{v.clear()});function re(e,n){let t=JSON.stringify([B(n),e]),r=v.get(t);if(r)return r;if(v.size>=_e){let s=v.keys().next().value;s!==void 0&&v.delete(s)}return r=fe(ue(e),n),v.set(t,r),r}var T=new Map,Re=500;function Ce(e){me(e);let n=B(e);if(T.has(n))return T.get(n);if(T.size>=Re){let i=T.keys().next().value;i!==void 0&&T.delete(i)}let t,r=n.split("-"),s=r.indexOf("u"),o=s===-1?-1:r.indexOf("ca",s+1);if(o!==-1&&o+1<r.length){let i=new Intl.DateTimeFormat(n).resolvedOptions().calendar;t=i==="gregory"?void 0:i}return T.set(n,t),t}function Ie(e,n){if(e==="Z"){/* c8 ignore start @preserve -- unreachable: lowercase tokens' regex
|
|
9
|
+
(OFFSET_SHAPES in pattern.ts) has no "Z" alternative at all, so
|
|
10
|
+
raw === 'Z' can only ever be reached when token is one of the
|
|
11
|
+
uppercase variants (X/XX/XXX). A lowercase token can't even
|
|
12
|
+
capture "Z" as `raw` in the first place. */if(n==="x"||n==="xx"||n==="xxx")throw new Error(`temporal-fmt: offset token "${n}" 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.`);/* c8 ignore stop @preserve */return"+00:00"}let t=e[0];/* c8 ignore start @preserve -- unreachable: raw is a regex-captured
|
|
13
|
+
group from an offset token, and every OFFSET_SHAPES pattern
|
|
14
|
+
(pattern.ts) is anchored to either "Z" or a leading [+-]. raw's
|
|
15
|
+
first character can never be anything else by the time it reaches
|
|
16
|
+
this function. */if(t!=="+"&&t!=="-")throw new Error(`temporal-fmt: offset "${e}" for token "${n}" doesn't start with "+", "-", or "Z".`);/* c8 ignore stop @preserve */let r=e.slice(1),s,o;if(r.length===2){/* c8 ignore start @preserve -- unreachable: each offset token's own
|
|
17
|
+
regex shape in OFFSET_SHAPES (pattern.ts) already gates which
|
|
18
|
+
body shapes it can capture. Only X and x ever match a 2-digit
|
|
19
|
+
body — XX/xx/XXX/xxx's regexes can't produce one — so this
|
|
20
|
+
mismatch can never actually fire through parse(). */if(n!=="X"&&n!=="x")throw new Error(`temporal-fmt: offset token "${n}" can't match "${e}" \u2014 it requires minutes, but "${e}" has none.`);/* c8 ignore stop @preserve */s=r,o="00"}else if(r.length===4){/* c8 ignore start @preserve -- unreachable, same reason as the
|
|
21
|
+
2-digit case above: XXX/xxx's regex requires a colon, so it can
|
|
22
|
+
never capture a 4-digit no-colon body in the first place. */if(n==="XXX"||n==="xxx")throw new Error(`temporal-fmt: offset token "${n}" can't match "${e}" \u2014 it requires a colon between hours and minutes (e.g. "${t}${r.slice(0,2)}:${r.slice(2)}").`);/* c8 ignore stop @preserve */s=r.slice(0,2),o=r.slice(2,4)}else if(r.length===5&&r[2]===":"){/* c8 ignore start @preserve -- unreachable, same reason again: only
|
|
23
|
+
XXX/xxx's regex can produce a colon-shaped body; X/x/XX/xx never
|
|
24
|
+
capture one. */if(n!=="XXX"&&n!=="xxx")throw new Error(`temporal-fmt: offset token "${n}" can't match "${e}" \u2014 it doesn't use a colon (use "${t}${r.slice(0,2)}${r.slice(3)}" instead).`);/* c8 ignore stop @preserve */s=r.slice(0,2),o=r.slice(3,5);/* c8 ignore start @preserve -- unreachable: every offset token's
|
|
25
|
+
regex only ever produces a body of length 2, length 4, or length 5
|
|
26
|
+
with a colon at index 2 (see OFFSET_SHAPES in pattern.ts) — no
|
|
27
|
+
shape falls outside those three cases, so this else arm can't be
|
|
28
|
+
taken through parse(). Kept as an exhaustiveness fallback so
|
|
29
|
+
hoursStr/minutesStr are assigned on every path TypeScript can see. */}else throw new Error(`temporal-fmt: offset "${e}" doesn't match the shape token "${n}" accepts.`);/* c8 ignore stop @preserve */let i=Number(s),u=Number(o);if(i>14)throw new z({actual:e,message:`temporal-fmt: offset hours ${i} in "${e}" out of range (max 14 \u2014 Kiritimati, Line Islands is +14:00).`});if(u>59)throw new z({actual:e,message:`temporal-fmt: offset minutes ${u} in "${e}" out of range (max 59).`});if(t==="+"&&i===14&&u!==0)throw new z({actual:e,message:`temporal-fmt: offset "${e}" exceeds the maximum supported UTC offset of +14:00.`});if(t==="-"&&i===12&&u!==0)throw new z({actual:e,message:`temporal-fmt: offset "${e}" exceeds the maximum supported negative UTC offset of -12:00.`});return`${t}${s}:${o}`}function d(e,n,t){e[n]=t}function Fe(e,n,t,r,s){let o=A(r);switch(n){case"yyyy":case"y":d(e,"year",Number(t));break;case"yy":d(e,"twoDigitYear",Number(t));break;case"MM":case"M":d(e,"month",Number(t));break;case"MMMM":d(e,"month",o.monthLong.indexOf(t)+1);break;case"MMM":d(e,"month",o.monthShort.indexOf(t)+1);break;case"dd":case"d":d(e,"day",Number(t));break;case"EEEE":d(e,"weekdayRaw",t),d(e,"weekdayExpected",o.weekdayLong.indexOf(t)+1);break;case"EEE":d(e,"weekdayRaw",t),d(e,"weekdayExpected",o.weekdayShort.indexOf(t)+1);break;case"HH":case"H":d(e,"hour",Number(t));break;case"hh":case"h":d(e,"hour12",Number(t));break;case"mm":case"m":d(e,"minute",Number(t));break;case"ss":case"s":d(e,"second",Number(t));break;case"S":case"SS":case"SSS":case"SSSS":case"SSSSS":case"SSSSSS":case"SSSSSSS":case"SSSSSSSS":case"SSSSSSSSS":{let i=Number(t.padEnd(9,"0"));d(e,"millisecond",Math.floor(i/1e6)),d(e,"microsecond",Math.floor(i/1e3)%1e3),d(e,"nanosecond",i%1e3);break}case"a":{let i=o.dayPeriod.findIndex(u=>u.toLowerCase()===t.toLowerCase());/* c8 ignore start @preserve -- unreachable: the 'a' token's regex
|
|
30
|
+
fragment (pattern.ts's alternation() over vocab.dayPeriod) can
|
|
31
|
+
only ever capture a case-insensitive match of one of
|
|
32
|
+
vocab.dayPeriod's own entries. Both the regex and this lookup
|
|
33
|
+
derive their vocab from the same `locale` via getLocaleVocab(),
|
|
34
|
+
so periodIndex can't come back negative through parse(). */if(i<0)throw new Error(`temporal-fmt: unknown day period "${t}" for locale "${r}".`);/* c8 ignore stop @preserve */d(e,"dayPeriodRaw",t),d(e,"isPM",i===1);break}case"zzz":d(e,"timeZoneId",t);break;case"X":case"XX":case"XXX":case"x":case"xx":case"xxx":d(e,"offsetString",Ie(t,n));break;case"Q":d(e,"quarter",Number(t));break;case"QQQ":d(e,"quarter",Number(t.slice(1)));break}}function le(e,n){let t=n.indexOf("d");if(t!==-1){let r=e.filter(s=>s[t]<=12);if(r.length>0)return r[0]}return e[0]}function Ze(e){if(e.year!==void 0&&e.twoDigitYear!==void 0)throw new l({message:'temporal-fmt: format string mixes a full-year token ("yyyy"/"y") with the two-digit "yy" token.'});if(e.year!==void 0)return e.year;if(e.twoDigitYear!==void 0)return e.twoDigitYear<=68?2e3+e.twoDigitYear:1900+e.twoDigitYear}function Le(e,n,t){if(e.hour!==void 0&&e.hour12!==void 0)throw new l({format:n,message:`temporal-fmt: format string "${n}" mixes a 24-hour token ("HH"/"H") with a 12-hour token ("hh"/"h").`});if(e.hour!==void 0){if(e.dayPeriodRaw!==void 0)throw new l({format:n,message:`temporal-fmt: format string "${n}" 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.`});return e.hour}if(e.hour12!==void 0){if(e.isPM===void 0)throw new l({format:n,message:`temporal-fmt: format string "${n}" uses a 12-hour token ("hh"/"h") without an "a" token, so parse() can't tell AM from PM.`});return e.hour12%12+(e.isPM?12:0)}}function se(e,n,t={}){if(e.length>1e3)throw new l({format:e,message:`temporal-fmt: format string exceeds maximum length of ${1e3} characters (got ${e.length}).`});if(n.length>1e5)throw new l({input:n,message:`temporal-fmt: input exceeds maximum length of ${1e5} characters (got ${n.length}).`});t.parseNumberingSystem&&(n=J(n,t));let r=t.locale??F,s=Ce(r),o=re(e,r),i=o.regex.exec(n);if(!i)throw new x({input:n,format:e,reason:"no valid pattern matches the format string and input shape",message:"temporal-fmt: no valid pattern matches the format string and input shape"});if(o.groups.length===0)throw new x({format:e,reason:`format string "${e}" has no tokens \u2014 nothing to parse into a value.`,message:`temporal-fmt: format string "${e}" has no tokens \u2014 nothing to parse into a value.`});for(let{name:c,token:w}of o.groups)if(w==="zzz"&&!W(i.groups[c]))throw new j({input:n,format:e,actual:i.groups[c],reason:"not a recognized IANA time zone identifier"});let u=new Map;for(let c of o.ambiguousRuns){let w=i.groups[c.groupName],b=ne(w,c.tokens);if(b.length===0)throw new x({input:n,format:e,reason:"no valid pattern matches the format string and input shape",message:"temporal-fmt: no valid pattern matches the format string and input shape"});if(b.length>1){if(!t.lenient)throw new q({input:n,format:e,message:`temporal-fmt: "${w}" in format string "${e}" is ambiguous \u2014 ${b.length} different ways to read tokens "${c.tokens.join("")}" (with no separator between them) are all individually valid (e.g. ${JSON.stringify(b[0])} vs ${JSON.stringify(b[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.`});let P=le(b,c.tokens);c.groupNames.forEach((K,Se)=>u.set(K,String(P[Se])))}else c.groupNames.forEach((P,K)=>u.set(P,String(b[0][K])))}let S={};for(let{name:c,token:w}of o.groups){let b=u.get(c)??i.groups[c];Fe(S,w,b,r,e)}let h=Ze(S),m=Le(S,e,r),{month:a,day:g,minute:f,second:p,millisecond:y,microsecond:D,nanosecond:he,timeZoneId:O,offsetString:k,weekdayExpected:U,weekdayRaw:pe,quarter:Q}=S,we=h!==void 0||a!==void 0||g!==void 0,$=h!==void 0&&a!==void 0&&g!==void 0;if(we&&!$)throw new M({format:e,message:`temporal-fmt: format string "${e}" has an incomplete date \u2014 year, month, and day tokens must all be present together.`});let R=m!==void 0||f!==void 0||p!==void 0||y!==void 0;if(O!==void 0&&!($&&R))throw new l({format:e,message:`temporal-fmt: format string "${e}" has a "zzz" token but needs a full date and time to build a ZonedDateTime.`});if(k!==void 0&&!($&&R))throw new l({format:e,message:`temporal-fmt: format string "${e}" has an offset token (X/XX/XXX/x/xx/xxx) but needs a full date and time to build a ZonedDateTime.`});if(U!==void 0&&!$)throw new l({format:e,message:`temporal-fmt: format string "${e}" has a weekday token ("EEEE"/"EEE") but needs a full date to validate it against.`});if(!$&&!R)throw new l({format:e,message:`temporal-fmt: format string "${e}" has no date or time tokens to parse.`});let C=I(),N={hour:m??0,minute:f??0,second:p??0,millisecond:y??0,microsecond:D??0,nanosecond:he??0},Y=s?{calendar:s}:{},G={overflow:"reject"},E;try{if(O!==void 0){let c={overflow:"reject",offset:"prefer"};if(E=C.ZonedDateTime.from({year:h,month:a,day:g,...N,...Y,timeZone:O,...k!==void 0?{offset:k}:{}},c),k!==void 0){let w=E;if(w.hour!==N.hour||w.minute!==N.minute||w.second!==N.second)throw new M({input:n,format:e,message:`"${O}" has no such wall-clock time on this date \u2014 it falls in a DST gap, not an ambiguous or valid instant.`});let P=w.offset;if(P!==k)throw new x({input:n,format:e,message:`has both a "zzz" zone (${O}) and an offset token (${k}), but the zone's actual offset at this date/time is ${P}, not ${k}.`})}}else{if(k!==void 0)throw new x({input:n,format:e,message:`format string "${e}" 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.`});$&&R?E=C.PlainDateTime.from({year:h,month:a,day:g,...N,...Y},G):$?E=C.PlainDate.from({year:h,month:a,day:g,...Y},G):E=C.PlainTime.from(N,G)}}catch(c){throw new M({input:n,format:e,message:`temporal-fmt: "${n}" doesn't describe a valid date/time for format "${e}": ${c.message}`})}if(U!==void 0){let c=E.dayOfWeek;if(c!==U){let w=A(r);throw new M({input:n,format:e,message:`temporal-fmt: "${pe}" doesn't match the actual weekday (${w.weekdayLong[c-1]}) for the parsed date.`})}}if(Q!==void 0&&a!==void 0){let c=Math.ceil(a/3);if(Q!==c)throw new M({format:e,message:`temporal-fmt: format string "${e}" contains a quarter token (Q/QQQ) whose value (Q${Q}) disagrees with the parsed month's actual quarter \u2014 month ${a} is in Q${c}.`})}return E}function He(e,n,t={}){try{return{ok:!0,value:se(e,n,t)}}catch(r){/* c8 ignore start @preserve */return r instanceof oe?{ok:!1,error:r}:{ok:!1,error:ae(r,{input:n,format:e})}}/* c8 ignore stop @preserve */}function Ue(e,n,t={}){try{return se(e,n,t)}catch{return}}function Qe(e,n,t={}){if(e.length>1e3)throw new l({format:e,message:`temporal-fmt: format string exceeds maximum length of ${1e3} characters (got ${e.length}).`});if(n.length>1e5)throw new l({input:n,message:`temporal-fmt: input exceeds maximum length of ${1e5} characters (got ${n.length}).`});t.parseNumberingSystem&&(n=J(n,t));let r=t.locale??F,s=re(e,r),o=s.regex.exec(n);if(!o)throw new x({input:n,format:e,reason:"no valid pattern matches the format string and input shape",message:"temporal-fmt: no valid pattern matches the format string and input shape"});if(s.groups.length===0)throw new x({format:e,reason:`format string "${e}" has no tokens \u2014 nothing to parse into a value.`,message:`temporal-fmt: format string "${e}" has no tokens \u2014 nothing to parse into a value.`});for(let{name:a,token:g}of s.groups)if(g==="zzz"&&!W(o.groups[a]))throw new j({input:n,format:e,actual:o.groups[a],reason:"not a recognized IANA time zone identifier"});let i=new Map;for(let a of s.ambiguousRuns){let g=o.groups[a.groupName],f=ne(g,a.tokens);if(f.length===0)throw new x({input:n,format:e,reason:"no valid pattern matches the format string and input shape",message:"temporal-fmt: no valid pattern matches the format string and input shape"});if(f.length>1){if(!t.lenient)throw new q({input:n,format:e,message:`temporal-fmt: "${g}" in format string "${e}" is ambiguous \u2014 ${f.length} different ways to read tokens "${a.tokens.join("")}" (with no separator between them) are all individually valid (e.g. ${JSON.stringify(f[0])} vs ${JSON.stringify(f[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.`});let p=le(f,a.tokens);a.groupNames.forEach((y,D)=>i.set(y,String(p[D])))}else a.groupNames.forEach((p,y)=>i.set(p,String(f[0][y])))}let u=[],h=o.indices?.groups,m=0;for(let{name:a,token:g}of s.groups){let f=i.get(a)??o.groups[a],p=!i.has(a)&&h?.[a],y=p?p[0]:(o.index??0)+m;u.push({token:g,raw:f,position:y}),m+=f.length}return u}function an(e,n={}){if(e.length>1e3)throw new l({format:e,message:`temporal-fmt: format string exceeds maximum length of ${1e3} characters (got ${e.length}).`});let t=n.locale??F,r=re(e,t);return{formatStr:e,pattern:r,parse(s,o={}){return se(e,s,{locale:t,...o})},safeParse(s,o={}){return He(e,s,{locale:t,...o})},tryParse(s,o={}){return Ue(e,s,{locale:t,...o})},parseToParts(s,o={}){return Qe(e,s,{locale:t,...o})}}}export{Te as a,ce as b,se as c,He as d,Ue as e,Qe as f,an as g};
|
|
35
|
+
//# sourceMappingURL=chunk-4JDAJSEM.js.map
|