@cosyte/dates 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6ba2afd: First release of `@cosyte/dates`: validate, render and convert the date parts the `@cosyte/*` parsers return, without adding precision a value did not state or guessing a time zone.
8
+
9
+ What you can depend on from this release: `validateParts`, `isValidParts` and `assertValidParts`, which report every fault in a value and name the rule each one broke; `precisionOf`; `toISO`, which renders ISO 8601 at the value's own precision; `toTemporal`, which returns the narrowest Temporal type that carries everything the value states; `toZonedDateTime` and `toInstant`, which need a time of day and a zone and never fall back to the host zone or to UTC; the re-exported `Temporal`; the shape of `DateParts`; the issue codes; and the four error classes, `DatePartsError`, `MissingZoneError`, `PrecisionError` and `AmbiguousLocalTimeError`.
10
+
11
+ What the version number promises: while it is below 1.0.0, a breaking change to any of the above ships in a new minor version, never a patch, and its entry here says what broke and what to do instead. A fix that changes nothing else a consumer relies on ships as a patch.
12
+
13
+ Not covered yet: parsing vendor date text (the parsers do that), two-digit years (never interpreted), arithmetic, comparison or formatting helpers (convert with `toTemporal()` and use Temporal for those), locale-aware output, years outside 1000 to 9999, calendars other than the proleptic Gregorian, and leap seconds.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cosyte
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ <a href="https://cosyte.com">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
4
+ <img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
5
+ </picture>
6
+ </a>
7
+
8
+ # @cosyte/dates
9
+
10
+ > Work with the dates the @cosyte/* parsers give you, without losing what the wire actually said.
11
+
12
+ [![npm version](https://img.shields.io/npm/v/@cosyte/dates.svg)](https://www.npmjs.com/package/@cosyte/dates)
13
+ [![CI](https://github.com/cosyte/dates/actions/workflows/ci.yml/badge.svg)](https://github.com/cosyte/dates/actions/workflows/ci.yml)
14
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/cosyte/dates/blob/main/LICENSE)
15
+ [![Node](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen.svg)](https://nodejs.org)
16
+
17
+ Date and time utilities for the @cosyte/* healthcare parsers: precision-preserving conversion and validation that never guesses a timezone.
18
+
19
+ ## Why this exists
20
+
21
+ Healthcare parsers hand back dates that are deliberately not JavaScript `Date`
22
+ objects. An HL7 v2 `|1970|` is a year, `|19880507|` is a calendar day, and a
23
+ value with no offset is the sender's local time by the standard's own words. The
24
+ parsers preserve exactly that and refuse to invent the rest, which is why they
25
+ carry no date library at all.
26
+
27
+ That fidelity leaves a gap: a developer still has to validate, compare and
28
+ convert those values, and the obvious tools all model an absolute instant. Reach
29
+ for `new Date('1988')` and you have silently acquired a month, a day, a midnight
30
+ and a timezone that nobody sent you. This package is the opt-in companion that
31
+ closes the gap without any of that, so the parsers stay zero-dependency and
32
+ nobody has their date library chosen for them.
33
+
34
+ ## Status
35
+
36
+ **0.1.0. The public API is settled and safe to depend on.** The exported
37
+ functions, the shape of `DateParts`, the issue codes and the error classes are
38
+ what this package intends to keep. While the version is below 1.0.0, a breaking
39
+ change to any of them ships in a new minor version, never a patch, and the
40
+ changelog says what broke and what to do instead.
41
+
42
+ Not covered at 0.1.0, and named so the boundary is not a surprise:
43
+
44
+ - **No parsing of vendor date text.** This package consumes parts that a
45
+ `@cosyte/*` parser already produced. Format strings and vendor token grammars
46
+ live in the parsers' profile systems, not here.
47
+ - **No two-digit years, ever.** `year` must be four digits. There is no century
48
+ window, no pivot, and no configuration that adds one.
49
+ - **No arithmetic, comparison or formatting helpers.** Convert with
50
+ `toTemporal()` and use Temporal for the rest.
51
+ - **No locale-aware or human-readable output.** `toISO()` renders ISO 8601 and
52
+ nothing else.
53
+ - **Years 1000 to 9999 only**, in the proleptic Gregorian calendar. No other
54
+ calendar system, and no leap seconds.
55
+
56
+ ## Install
57
+
58
+ ```sh
59
+ pnpm add @cosyte/dates
60
+ ```
61
+
62
+ Node `>=22.0.0`. ESM only: this package ships no CommonJS build, so `require()`
63
+ will not load it.
64
+
65
+ It carries exactly one runtime dependency, [`temporal-polyfill`][polyfill],
66
+ because `Temporal.PlainDate` is the only widely available type that can hold a
67
+ calendar date with no time of day and no zone. `Temporal` is re-exported from
68
+ this package, so a consumer works with the same objects rather than installing a
69
+ second copy.
70
+
71
+ [polyfill]: https://www.npmjs.com/package/temporal-polyfill
72
+
73
+ ## Usage
74
+
75
+ ```js
76
+ import { precisionOf, toISO, toInstant, toTemporal, validateParts } from '@cosyte/dates';
77
+
78
+ // A date of birth that arrived as a year and nothing more. It stays a year.
79
+ const birthYear = { year: 1988 };
80
+ console.log(toISO(birthYear), '| precision:', precisionOf(birthYear));
81
+
82
+ // A calendar date. No midnight is invented, and no zone is attached.
83
+ const admitted = { year: 1988, month: 5, day: 7 };
84
+ console.log(toISO(admitted), '| Temporal:', toTemporal(admitted).constructor.name);
85
+
86
+ // A timestamp whose sender stated its offset. The offset is kept exactly.
87
+ const collected = { year: 1988, month: 5, day: 7, hour: 13, minute: 45, offsetMinutes: 330 };
88
+ console.log(toISO(collected), '->', toInstant(collected).toString());
89
+
90
+ // The same timestamp with an unknown offset. Nothing guesses one.
91
+ const unknownZone = { year: 1988, month: 5, day: 7, hour: 13, minute: 45 };
92
+ try {
93
+ toInstant(unknownZone);
94
+ } catch (error) {
95
+ console.log(error.name);
96
+ }
97
+
98
+ // A date that does not exist is refused, and says which rule it broke.
99
+ const result = validateParts({ year: 2025, month: 2, day: 29 });
100
+ console.log(result.valid ? 'valid' : result.issues[0].message);
101
+ ```
102
+
103
+ ```text
104
+ 1988 | precision: year
105
+ 1988-05-07 | Temporal: PlainDate
106
+ 1988-05-07T13:45+05:30 -> 1988-05-07T08:15:00Z
107
+ MissingZoneError
108
+ day 29 does not exist: February 2025 has 28 days, because 2025 is not a leap year in the proleptic Gregorian calendar
109
+ ```
110
+
111
+ That output is identical under every host `TZ`, and
112
+ `test/readme.test.ts` runs this exact block against the built package and
113
+ compares it character for character.
114
+
115
+ More runnable programs, one for each job this package does, are in
116
+ [`examples/`](https://github.com/cosyte/dates/tree/main/examples), and the full
117
+ guide is at [docs.cosyte.com/dates](https://docs.cosyte.com/dates).
118
+
119
+ ## PHI and safety
120
+
121
+ A date of birth is PHI, and a wrong conversion is the quiet kind of defect: it
122
+ returns a plausible date rather than an error, and a consumer that has already
123
+ written it into a record does not learn of it from a later fix. What this
124
+ package does about that:
125
+
126
+ - **It writes nothing, anywhere.** No logging, no telemetry, no files, no
127
+ network. Nothing is written to stdout or stderr, on any code path including
128
+ every error path. `test/package-contract.test.ts` proves it by running every
129
+ public entry point in a child process and asserting both streams are empty and
130
+ that nothing was written to the working directory.
131
+ - **It retains nothing.** Every function is pure and holds no state between
132
+ calls. A value passed in is not cached, memoised or referenced after return.
133
+ - **It refuses rather than guesses.** No century window, no host timezone
134
+ fallback, no midnight for a date with no time, no picking one side of a
135
+ daylight-saving transition, no rounding a sub-second value to make it fit.
136
+ Each of those raises a named error instead.
137
+ - **Diagnostics quote one component, never the whole value.** A validation
138
+ message names the component at fault and the value of THAT component
139
+ (`month must be an integer in 1-12 ...; received the number 13`). It never
140
+ assembles the full date into an error string.
141
+
142
+ What the consumer still owns: whether to log an error message that may contain
143
+ one component of a patient's date of birth, how the parts got there, and where
144
+ the converted value goes next. This package is a pure function in the middle.
145
+
146
+ ## API
147
+
148
+ | export | what it does |
149
+ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
150
+ | `validateParts(value)` | Checks a value against the contract. Never throws. Returns every fault, not just the first. |
151
+ | `isValidParts(value)` | The same check as a type guard. |
152
+ | `assertValidParts(value)` | Returns the recognised components, or throws `DatePartsError`. |
153
+ | `precisionOf(value)` | The least significant present component: `'year'` through `'fraction'`. |
154
+ | `toISO(value)` | ISO 8601 at the value's own precision. `1988`, `1988-05`, `1988-05-07T13:45+05:30`. |
155
+ | `toTemporal(value)` | The narrowest Temporal type that carries everything the value states: `PlainYearMonth`, `PlainDate`, `PlainDateTime`, or `ZonedDateTime` when the value carries an offset. |
156
+ | `toZonedDateTime(value, options?)` | The value placed in a stated zone. Needs a time of day and a zone. |
157
+ | `toInstant(value, options?)` | The absolute instant, same requirements. |
158
+ | `Temporal` | The Temporal implementation this package converts into. |
159
+
160
+ Errors: `DatePartsError` (carries the same `issues` the validator reports),
161
+ `MissingZoneError`, `PrecisionError`, `AmbiguousLocalTimeError`.
162
+
163
+ The caveats that bite:
164
+
165
+ - A value's OWN `offsetMinutes` always wins. `options` apply only when the
166
+ offset is unknown, so passing a `timeZone` never re-zones data the sender
167
+ already qualified.
168
+ - `toTemporal()` throws for a year-only value, because Temporal has no year-only
169
+ type and this package will not invent a month to reach one.
170
+ - An absent `offsetMinutes` means UNKNOWN. It does not mean UTC, and it does not
171
+ mean the host zone.
172
+
173
+ ## Compatibility
174
+
175
+ The input shape is the spec-native parts value the `@cosyte/*` parsers produce:
176
+ `month` is 1 to 12, not JavaScript's 0 to 11, and components run from `year`
177
+ downward with no gaps. Handled: year, month, day, hour, minute, second,
178
+ sub-second down to one nanosecond, and a signed offset in minutes.
179
+
180
+ `fraction` must be a whole number of nanoseconds, which is nine decimal places of
181
+ a second and Temporal's own floor. A value carrying finer detail is REFUSED with
182
+ `finer-than-nanosecond` rather than rounded to fit, because a silent round is the
183
+ class of change this package exists to not make.
184
+
185
+ Known NOT handled, deliberately: two-digit years, leap seconds, calendars other
186
+ than the proleptic Gregorian, years outside 1000 to 9999, sub-second detail finer
187
+ than one nanosecond, an offset on a value that states no time of day, and any
188
+ zone abbreviation such as `CST` (they are ambiguous across regions, so an IANA
189
+ identifier or a numeric offset is required).
190
+
191
+ ## Contributing
192
+
193
+ Issues and pull requests are welcome at
194
+ [github.com/cosyte/dates](https://github.com/cosyte/dates). A contribution has
195
+ to clear the same gate CI runs on every push: `pnpm run build`, `pnpm run
196
+ attw`, `pnpm run typecheck`, `pnpm run lint`, `pnpm run test`, `pnpm run
197
+ examples`, and `bash scripts/check-no-emdash.sh`. New behaviour needs a test, and anything
198
+ touching conversion needs one that pins the exact output rather than a shape.
199
+
200
+ ## License
201
+
202
+ MIT, Cosyte. See [LICENSE](https://github.com/cosyte/dates/blob/main/LICENSE).
@@ -0,0 +1,246 @@
1
+ import { Temporal } from 'temporal-polyfill';
2
+ export { Temporal } from 'temporal-polyfill';
3
+
4
+ /**
5
+ * The date-parts value every `@cosyte/*` parser hands back, and the only shape
6
+ * this package accepts.
7
+ *
8
+ * Two rules make it a contract rather than a bag of numbers.
9
+ *
10
+ * 1. CONTIGUITY. Components are present from the most significant downward with
11
+ * no gaps. `{ year, month }` is legal; `{ year, day }` is not.
12
+ * 2. PRECISION IS WHAT IS PRESENT. The least significant present component IS
13
+ * the precision of the value. Nothing here adds a component that was not
14
+ * supplied, and an absent `offsetMinutes` means the offset is UNKNOWN, which
15
+ * is not the same as UTC and not the same as the host zone.
16
+ *
17
+ * `month` is SPEC-NATIVE 1-12, not JavaScript's 0-11.
18
+ */
19
+ interface DateParts {
20
+ /** Four digits, 1000-9999. Required. Two-digit years are never interpreted. */
21
+ readonly year?: number;
22
+ /** 1-12, spec-native. January is 1. */
23
+ readonly month?: number;
24
+ /** 1-31, and a day that exists in the given month of the given year. */
25
+ readonly day?: number;
26
+ /** 0-23. */
27
+ readonly hour?: number;
28
+ /** 0-59. */
29
+ readonly minute?: number;
30
+ /** 0-59. Leap seconds are not representable. */
31
+ readonly second?: number;
32
+ /** Sub-second precision as a fraction of one second: `0 <= fraction < 1`. */
33
+ readonly fraction?: number;
34
+ /**
35
+ * Signed minutes from UTC, -1439 to 1439. ABSENT means the offset is unknown.
36
+ * It never means zero, and it is never filled in from the host zone.
37
+ */
38
+ readonly offsetMinutes?: number;
39
+ }
40
+ /** The precision ladder, most significant first. */
41
+ declare const PRECISION_LADDER: readonly ["year", "month", "day", "hour", "minute", "second", "fraction"];
42
+ /** The least significant component present in a value IS its precision. */
43
+ type Precision = (typeof PRECISION_LADDER)[number];
44
+ /** Every component name the contract defines. */
45
+ type PartName = Precision | 'offsetMinutes';
46
+ /** What a diagnostic says went wrong. */
47
+ type IssueCode =
48
+ /** The argument is not a date-parts object at all. */
49
+ 'not-a-parts-object'
50
+ /** A more significant component is absent while a less significant one is present. */
51
+ | 'missing-component'
52
+ /** Reading the component threw, so its value could not be seen at all. */
53
+ | 'unreadable'
54
+ /** The component is present but is not a number. */
55
+ | 'not-a-number'
56
+ /** The component is a number but is `NaN` or infinite. */
57
+ | 'not-finite'
58
+ /** The component must be a whole number and is not. */
59
+ | 'not-an-integer'
60
+ /** The component is outside the range the contract gives it. */
61
+ | 'out-of-range'
62
+ /** The year is not four digits. No century window is ever applied. */
63
+ | 'year-not-four-digits'
64
+ /** The sub-second value carries detail below nanosecond resolution. */
65
+ | 'finer-than-nanosecond'
66
+ /** An offset was given for a value that states no time of day. */
67
+ | 'offset-without-time-of-day'
68
+ /** The components are each in range but together denote no real date. */
69
+ | 'no-such-calendar-date';
70
+ /** One thing wrong with one component, named. */
71
+ interface ValidationIssue {
72
+ /** The component at fault, or `'value'` when the argument itself is wrong. */
73
+ readonly component: PartName | 'value';
74
+ readonly code: IssueCode;
75
+ /** Human-readable, names the component and the range or calendar rule broken. */
76
+ readonly message: string;
77
+ }
78
+ /** A validated value, carrying only the recognised components that were present. */
79
+ interface ValidPartsResult {
80
+ readonly valid: true;
81
+ readonly parts: DateParts;
82
+ readonly precision: Precision;
83
+ }
84
+ /** An invalid value, with every fault found rather than only the first. */
85
+ interface InvalidPartsResult {
86
+ readonly valid: false;
87
+ readonly issues: readonly ValidationIssue[];
88
+ }
89
+ type ValidationResult = ValidPartsResult | InvalidPartsResult;
90
+ /**
91
+ * How to place a value that carries no offset of its own onto the timeline.
92
+ *
93
+ * A value that DOES carry `offsetMinutes` is never re-zoned: its own offset is
94
+ * what the sender said, and overriding it here would move the instant.
95
+ */
96
+ interface ZoneOptions {
97
+ /** An IANA time zone identifier, for example `'America/New_York'`. */
98
+ readonly timeZone?: string;
99
+ /** A fixed offset in signed minutes from UTC, -1439 to 1439. */
100
+ readonly offsetMinutes?: number;
101
+ }
102
+
103
+ /**
104
+ * A conversion was asked for a value that validation reports invalid.
105
+ *
106
+ * `issues` is the SAME array `validateParts()` returns for that value, so a
107
+ * caller that catches this never has to validate again to learn what was wrong,
108
+ * and a conversion never returns a coerced or clamped stand-in.
109
+ */
110
+ declare class DatePartsError extends Error {
111
+ readonly issues: readonly ValidationIssue[];
112
+ constructor(issues: readonly ValidationIssue[]);
113
+ }
114
+ /**
115
+ * An absolute instant was asked for, and nothing said which zone the local time
116
+ * is in.
117
+ *
118
+ * This package never falls back to the host timezone, to UTC, or to any other
119
+ * default, because a fallback turns an unknown into a plausible wrong answer.
120
+ */
121
+ declare class MissingZoneError extends Error {
122
+ constructor(message: string);
123
+ }
124
+ /**
125
+ * The value does not carry enough precision for what was asked, and supplying
126
+ * the missing components would mean inventing them.
127
+ */
128
+ declare class PrecisionError extends Error {
129
+ /** The precision the value actually has. */
130
+ readonly precision: string;
131
+ constructor(message: string, precision: string);
132
+ }
133
+ /**
134
+ * A local time was placed in a named zone where it does not exist, or exists
135
+ * twice.
136
+ *
137
+ * Both cases are resolved by REFUSING rather than by picking. A daylight-saving
138
+ * transition is exactly where a silent choice shifts a clinical timeline by an
139
+ * hour, so the caller is told and decides.
140
+ */
141
+ declare class AmbiguousLocalTimeError extends Error {
142
+ constructor(message: string);
143
+ }
144
+
145
+ /**
146
+ * Check a value against the date-parts contract.
147
+ *
148
+ * Every fault is reported, not only the first, and every diagnostic names the
149
+ * component at fault together with the range or calendar rule it broke. Nothing
150
+ * is coerced, clamped or inferred: a value that does not satisfy the contract
151
+ * comes back invalid rather than corrected.
152
+ *
153
+ * This function never throws, for any argument at all. Hostile input (`null`, a
154
+ * string, a JavaScript `Date`, `NaN`, `Infinity`, a non-integer component, a
155
+ * component that throws when it is read) is reported, not raised.
156
+ *
157
+ * Components are looked up by name, so an object carrying them on its prototype
158
+ * (a class instance with accessors, which is a shape a parser may well hand
159
+ * over) is read the same way a plain object literal is.
160
+ */
161
+ declare function validateParts(value: unknown): ValidationResult;
162
+ /** A type guard over {@link validateParts}, for callers that want a boolean. */
163
+ declare function isValidParts(value: unknown): value is DateParts;
164
+ /**
165
+ * Validate and return the recognised components, or throw {@link DatePartsError}
166
+ * carrying the same diagnostics {@link validateParts} would have reported.
167
+ */
168
+ declare function assertValidParts(value: unknown): DateParts;
169
+ /**
170
+ * The precision of a value: its least significant present component.
171
+ *
172
+ * Throws {@link DatePartsError} for a value that does not satisfy the contract,
173
+ * because an invalid value has no precision to report.
174
+ */
175
+ declare function precisionOf(value: unknown): Precision;
176
+
177
+ /** The narrowest Temporal type that carries everything a value states, and nothing more. */
178
+ type TemporalValue = Temporal.PlainYearMonth | Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime;
179
+ /**
180
+ * Render a value as an ISO 8601 string AT ITS OWN PRECISION.
181
+ *
182
+ * A year-only value renders `1988`; a month-only value renders `1988-05`. No
183
+ * component the value did not state is ever added, so nothing here turns a year
184
+ * of birth into the first of January.
185
+ *
186
+ * The result is a pure function of the parts. It does not read the host clock or
187
+ * the host time zone, so it is identical under every `TZ`.
188
+ *
189
+ * @throws {DatePartsError} if the value does not satisfy the contract.
190
+ */
191
+ declare function toISO(value: unknown): string;
192
+ /**
193
+ * Convert to the narrowest Temporal type that carries everything the value
194
+ * states, and nothing it does not.
195
+ *
196
+ * | precision of the value | result |
197
+ * |---|---|
198
+ * | `month` | `Temporal.PlainYearMonth` |
199
+ * | `day` | `Temporal.PlainDate`, with no time of day and no zone |
200
+ * | `hour` and finer, no offset | `Temporal.PlainDateTime`, with no zone |
201
+ * | `hour` and finer, with `offsetMinutes` | `Temporal.ZonedDateTime` at that exact offset |
202
+ *
203
+ * A year-only value has no Temporal counterpart and raises
204
+ * {@link PrecisionError} rather than acquiring a month. Use {@link toISO} for
205
+ * those, and {@link precisionOf} to branch.
206
+ *
207
+ * Components below the value's own precision are zero in the Temporal object,
208
+ * because Temporal has no partial type below `PlainYearMonth`. The value's real
209
+ * precision is still available from {@link precisionOf}; it is not lost, it just
210
+ * cannot be carried by the returned object.
211
+ *
212
+ * @throws {DatePartsError} if the value does not satisfy the contract.
213
+ * @throws {PrecisionError} for a year-only value.
214
+ */
215
+ declare function toTemporal(value: unknown): TemporalValue;
216
+ /**
217
+ * Place a value on the timeline in a stated zone.
218
+ *
219
+ * Requires a time of day. A day-precision value is a calendar date, and turning
220
+ * one into a zoned instant means choosing midnight, which is a component the
221
+ * value does not carry. Use {@link toTemporal} for those.
222
+ *
223
+ * Requires a zone. If the value carries `offsetMinutes` that offset is used and
224
+ * PRESERVED EXACTLY, never normalised to UTC or to the host zone, and `options`
225
+ * are not consulted. Otherwise `options.timeZone` or `options.offsetMinutes`
226
+ * must say which zone the local time is in.
227
+ *
228
+ * A local time that a named zone skips or repeats across a daylight-saving
229
+ * transition raises {@link AmbiguousLocalTimeError} rather than resolving to
230
+ * one of the two candidates, because a silent pick shifts the value by an hour.
231
+ *
232
+ * @throws {DatePartsError} if the value does not satisfy the contract.
233
+ * @throws {PrecisionError} if the value states no time of day.
234
+ * @throws {MissingZoneError} if nothing says which zone applies.
235
+ * @throws {AmbiguousLocalTimeError} at a daylight-saving transition.
236
+ */
237
+ declare function toZonedDateTime(value: unknown, options?: ZoneOptions): Temporal.ZonedDateTime;
238
+ /**
239
+ * The absolute instant a value denotes, in a stated zone.
240
+ *
241
+ * Same requirements as {@link toZonedDateTime}: a time of day, and a zone that
242
+ * something states. Nothing is defaulted.
243
+ */
244
+ declare function toInstant(value: unknown, options?: ZoneOptions): Temporal.Instant;
245
+
246
+ export { AmbiguousLocalTimeError, type DateParts, DatePartsError, type InvalidPartsResult, type IssueCode, MissingZoneError, PRECISION_LADDER, type PartName, type Precision, PrecisionError, type TemporalValue, type ValidPartsResult, type ValidationIssue, type ValidationResult, type ZoneOptions, assertValidParts, isValidParts, precisionOf, toISO, toInstant, toTemporal, toZonedDateTime, validateParts };
package/dist/index.js ADDED
@@ -0,0 +1,484 @@
1
+ import { Temporal } from 'temporal-polyfill';
2
+ export { Temporal } from 'temporal-polyfill';
3
+
4
+ // src/types.ts
5
+ var PRECISION_LADDER = [
6
+ "year",
7
+ "month",
8
+ "day",
9
+ "hour",
10
+ "minute",
11
+ "second",
12
+ "fraction"
13
+ ];
14
+
15
+ // src/errors.ts
16
+ var DatePartsError = class extends Error {
17
+ issues;
18
+ constructor(issues) {
19
+ super(`invalid date parts: ${issues.map((issue) => issue.message).join("; ")}`);
20
+ this.name = "DatePartsError";
21
+ this.issues = issues;
22
+ }
23
+ };
24
+ var MissingZoneError = class extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "MissingZoneError";
28
+ }
29
+ };
30
+ var PrecisionError = class extends Error {
31
+ /** The precision the value actually has. */
32
+ precision;
33
+ constructor(message, precision) {
34
+ super(message);
35
+ this.name = "PrecisionError";
36
+ this.precision = precision;
37
+ }
38
+ };
39
+ var AmbiguousLocalTimeError = class extends Error {
40
+ constructor(message) {
41
+ super(message);
42
+ this.name = "AmbiguousLocalTimeError";
43
+ }
44
+ };
45
+
46
+ // src/fraction.ts
47
+ var NANOSECOND_DIGITS = 9;
48
+ var PLAIN_DECIMAL = /^0(?:\.(\d+))?$/;
49
+ var EXPONENTIAL_DECIMAL = /^(\d)(?:\.(\d+))?e-(\d+)$/;
50
+ function fractionDigits(fraction) {
51
+ const text = String(fraction);
52
+ const plain = PLAIN_DECIMAL.exec(text);
53
+ if (plain !== null) return plain[1] ?? "";
54
+ const exponential = EXPONENTIAL_DECIMAL.exec(text);
55
+ if (exponential === null) return void 0;
56
+ const lead = exponential[1] ?? "";
57
+ const rest = exponential[2] ?? "";
58
+ const exponent = Number(exponential[3]);
59
+ return "0".repeat(exponent - 1) + lead + rest;
60
+ }
61
+ function nanosecondsOf(fraction) {
62
+ const digits = fractionDigits(fraction);
63
+ if (digits === void 0 || digits.length > NANOSECOND_DIGITS) return void 0;
64
+ return digits === "" ? 0 : Number(digits.padEnd(NANOSECOND_DIGITS, "0"));
65
+ }
66
+ function readFraction(fraction) {
67
+ if (!Number.isFinite(fraction) || fraction < 0 || fraction >= 1) {
68
+ return {
69
+ ok: false,
70
+ issue: {
71
+ component: "fraction",
72
+ code: "out-of-range",
73
+ message: `fraction is a share of one second and must satisfy 0 <= fraction < 1; received the number ${String(fraction)}`
74
+ }
75
+ };
76
+ }
77
+ const nanoseconds = nanosecondsOf(fraction);
78
+ if (nanoseconds === void 0) {
79
+ return {
80
+ ok: false,
81
+ issue: {
82
+ component: "fraction",
83
+ code: "finer-than-nanosecond",
84
+ message: `fraction carries detail finer than one nanosecond, which this package will not round away; received the number ${String(fraction)}. One nanosecond is nine decimal places of a second, and it is the finest value Temporal can hold`
85
+ }
86
+ };
87
+ }
88
+ return { ok: true, nanoseconds };
89
+ }
90
+
91
+ // src/validate.ts
92
+ var ALL_PARTS = [...PRECISION_LADDER, "offsetMinutes"];
93
+ var INTEGER_RANGES = {
94
+ year: [1e3, 9999],
95
+ month: [1, 12],
96
+ day: [1, 31],
97
+ hour: [0, 23],
98
+ minute: [0, 59],
99
+ second: [0, 59],
100
+ offsetMinutes: [-1439, 1439]
101
+ };
102
+ var RANGE_NOTES = {
103
+ month: " (spec-native, January is 1, not 0)",
104
+ offsetMinutes: " (signed minutes from UTC)"
105
+ };
106
+ var MONTH_NAMES = [
107
+ "January",
108
+ "February",
109
+ "March",
110
+ "April",
111
+ "May",
112
+ "June",
113
+ "July",
114
+ "August",
115
+ "September",
116
+ "October",
117
+ "November",
118
+ "December"
119
+ ];
120
+ var MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
121
+ function isLeapYear(year) {
122
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
123
+ }
124
+ function daysInMonth(year, month) {
125
+ if (month === 2) return isLeapYear(year) ? 29 : 28;
126
+ return MONTH_LENGTHS[month - 1] ?? 0;
127
+ }
128
+ function monthName(month) {
129
+ return MONTH_NAMES[month - 1] ?? `month ${String(month)}`;
130
+ }
131
+ function describe(value) {
132
+ if (value === null) return "null";
133
+ if (value === void 0) return "undefined";
134
+ if (typeof value === "number") {
135
+ if (Number.isNaN(value)) return "NaN";
136
+ if (!Number.isFinite(value)) return value > 0 ? "Infinity" : "-Infinity";
137
+ return `the number ${String(value)}`;
138
+ }
139
+ if (typeof value === "string") return "a string";
140
+ if (typeof value === "boolean") return `the boolean ${String(value)}`;
141
+ if (typeof value === "bigint") return "a bigint";
142
+ if (typeof value === "symbol") return "a symbol";
143
+ if (typeof value === "function") return "a function";
144
+ if (Array.isArray(value)) return "an array";
145
+ return "an object";
146
+ }
147
+ function invalid(issues) {
148
+ return { valid: false, issues };
149
+ }
150
+ function readComponent(source, name) {
151
+ try {
152
+ return { ok: true, raw: source[name] };
153
+ } catch {
154
+ return {
155
+ ok: false,
156
+ issue: {
157
+ component: name,
158
+ code: "unreadable",
159
+ message: `${name} could not be read: the property threw when accessed, so this package cannot see what value it holds and will not guess one`
160
+ }
161
+ };
162
+ }
163
+ }
164
+ function validateParts(value) {
165
+ if (typeof value !== "object" || value === null) {
166
+ return invalid([
167
+ {
168
+ component: "value",
169
+ code: "not-a-parts-object",
170
+ message: `a date-parts value must be an object carrying the components the contract names; received ${describe(value)}`
171
+ }
172
+ ]);
173
+ }
174
+ if (value instanceof Date) {
175
+ return invalid([
176
+ {
177
+ component: "value",
178
+ code: "not-a-parts-object",
179
+ message: "a JavaScript Date is an absolute instant, not a date-parts value: it cannot express a year-only or month-only date, and it carries no offset of its own. Pass the parts the parser returned"
180
+ }
181
+ ]);
182
+ }
183
+ if (Array.isArray(value)) {
184
+ return invalid([
185
+ {
186
+ component: "value",
187
+ code: "not-a-parts-object",
188
+ message: "a date-parts value must be an object with named components; received an array, whose positions this package will not guess at"
189
+ }
190
+ ]);
191
+ }
192
+ const source = value;
193
+ const issues = [];
194
+ const present = /* @__PURE__ */ new Set();
195
+ const values = {};
196
+ for (const name of ALL_PARTS) {
197
+ const read = readComponent(source, name);
198
+ if (!read.ok) {
199
+ present.add(name);
200
+ issues.push(read.issue);
201
+ continue;
202
+ }
203
+ const raw = read.raw;
204
+ if (raw === void 0) continue;
205
+ present.add(name);
206
+ if (typeof raw !== "number") {
207
+ issues.push({
208
+ component: name,
209
+ code: "not-a-number",
210
+ message: `${name} must be a number; received ${describe(raw)}`
211
+ });
212
+ continue;
213
+ }
214
+ if (!Number.isFinite(raw)) {
215
+ issues.push({
216
+ component: name,
217
+ code: "not-finite",
218
+ message: `${name} must be a finite number; received ${describe(raw)}`
219
+ });
220
+ continue;
221
+ }
222
+ if (name === "fraction") {
223
+ const reading = readFraction(raw);
224
+ if (reading.ok) values.fraction = raw;
225
+ else issues.push(reading.issue);
226
+ continue;
227
+ }
228
+ if (!Number.isInteger(raw)) {
229
+ issues.push({
230
+ component: name,
231
+ code: "not-an-integer",
232
+ message: `${name} must be a whole number; received the number ${String(raw)}`
233
+ });
234
+ continue;
235
+ }
236
+ const [low, high] = INTEGER_RANGES[name];
237
+ if (raw < low || raw > high) {
238
+ issues.push(
239
+ name === "year" ? {
240
+ component: "year",
241
+ code: "year-not-four-digits",
242
+ message: `year must be a four-digit year in ${String(low)}-${String(high)}; received the number ${String(raw)}. This package applies no century window and never interprets a two-digit year`
243
+ } : {
244
+ component: name,
245
+ code: "out-of-range",
246
+ message: `${name} must be an integer in ${String(low)}-${String(high)}${RANGE_NOTES[name] ?? ""}; received the number ${String(raw)}`
247
+ }
248
+ );
249
+ continue;
250
+ }
251
+ values[name] = raw;
252
+ }
253
+ checkContiguity(present, issues);
254
+ checkOffsetHasTimeOfDay(present, issues);
255
+ checkCalendarDate(values, issues);
256
+ if (issues.length > 0) return invalid(issues);
257
+ const parts = {};
258
+ for (const name of ALL_PARTS) {
259
+ const held = values[name];
260
+ if (held !== void 0) parts[name] = held;
261
+ }
262
+ return { valid: true, parts, precision: deepestPresent(present) };
263
+ }
264
+ function deepestPresent(present) {
265
+ let deepest;
266
+ for (const name of PRECISION_LADDER) {
267
+ if (present.has(name)) deepest = name;
268
+ }
269
+ return deepest;
270
+ }
271
+ function checkContiguity(present, issues) {
272
+ const deepest = deepestPresent(present);
273
+ if (deepest === void 0) {
274
+ issues.push({
275
+ component: "year",
276
+ code: "missing-component",
277
+ message: "year is missing: a date-parts value must carry at least a year, and this package never infers one"
278
+ });
279
+ return;
280
+ }
281
+ const deepestIndex = PRECISION_LADDER.indexOf(deepest);
282
+ for (let index = 0; index < deepestIndex; index += 1) {
283
+ const name = PRECISION_LADDER[index];
284
+ if (name === void 0 || present.has(name)) continue;
285
+ issues.push({
286
+ component: name,
287
+ code: "missing-component",
288
+ message: `${name} is missing while ${deepest} is present: components run from year downward with no gaps, and this package never infers the one you left out`
289
+ });
290
+ }
291
+ }
292
+ function checkOffsetHasTimeOfDay(present, issues) {
293
+ if (!present.has("offsetMinutes") || present.has("hour")) return;
294
+ issues.push({
295
+ component: "offsetMinutes",
296
+ code: "offset-without-time-of-day",
297
+ message: "offsetMinutes is present but the value states no time of day: an offset qualifies a clock time, it cannot be rendered without one, and dropping it silently is not an option this package takes"
298
+ });
299
+ }
300
+ function checkCalendarDate(values, issues) {
301
+ const { year, month, day } = values;
302
+ if (year === void 0 || month === void 0 || day === void 0) return;
303
+ const limit = daysInMonth(year, month);
304
+ if (day <= limit) return;
305
+ const leapNote = month === 2 ? `, because ${String(year)} is ${isLeapYear(year) ? "a" : "not a"} leap year in the proleptic Gregorian calendar` : "";
306
+ issues.push({
307
+ component: "day",
308
+ code: "no-such-calendar-date",
309
+ message: `day ${String(day)} does not exist: ${monthName(month)} ${String(year)} has ${String(limit)} days${leapNote}`
310
+ });
311
+ }
312
+ function isValidParts(value) {
313
+ return validateParts(value).valid;
314
+ }
315
+ function assertValidParts(value) {
316
+ const result = validateParts(value);
317
+ if (!result.valid) throw new DatePartsError(result.issues);
318
+ return result.parts;
319
+ }
320
+ function precisionOf(value) {
321
+ const result = validateParts(value);
322
+ if (!result.valid) throw new DatePartsError(result.issues);
323
+ return result.precision;
324
+ }
325
+ function check(value) {
326
+ const result = validateParts(value);
327
+ if (!result.valid) throw new DatePartsError(result.issues);
328
+ return { parts: result.parts, precision: result.precision };
329
+ }
330
+ function atLeast(precision, floor) {
331
+ return PRECISION_LADDER.indexOf(precision) >= PRECISION_LADDER.indexOf(floor);
332
+ }
333
+ function pad(value, width) {
334
+ return String(value).padStart(width, "0");
335
+ }
336
+ function formatOffset(offsetMinutes) {
337
+ const sign = offsetMinutes < 0 ? "-" : "+";
338
+ const absolute = Math.abs(offsetMinutes);
339
+ return `${sign}${pad(Math.trunc(absolute / 60), 2)}:${pad(absolute % 60, 2)}`;
340
+ }
341
+ function nanosecondsOf2(fraction) {
342
+ const reading = readFraction(fraction);
343
+ if (!reading.ok) throw new DatePartsError([reading.issue]);
344
+ return reading.nanoseconds;
345
+ }
346
+ function formatFraction(fraction) {
347
+ const digits = pad(nanosecondsOf2(fraction), 9).replace(/0+$/, "");
348
+ return `.${digits === "" ? "0" : digits}`;
349
+ }
350
+ function subSecondFields(fraction) {
351
+ if (fraction === void 0) return {};
352
+ const nanoseconds = nanosecondsOf2(fraction);
353
+ return {
354
+ millisecond: Math.floor(nanoseconds / 1e6),
355
+ microsecond: Math.floor(nanoseconds % 1e6 / 1e3),
356
+ nanosecond: nanoseconds % 1e3
357
+ };
358
+ }
359
+ function toISO(value) {
360
+ const { parts, precision } = check(value);
361
+ const suffix = parts.offsetMinutes === void 0 ? "" : formatOffset(parts.offsetMinutes);
362
+ let out = pad(parts.year ?? 0, 4);
363
+ if (precision === "year") return out;
364
+ out += `-${pad(parts.month ?? 0, 2)}`;
365
+ if (precision === "month") return out;
366
+ out += `-${pad(parts.day ?? 0, 2)}`;
367
+ if (precision === "day") return out;
368
+ out += `T${pad(parts.hour ?? 0, 2)}`;
369
+ if (precision === "hour") return out + suffix;
370
+ out += `:${pad(parts.minute ?? 0, 2)}`;
371
+ if (precision === "minute") return out + suffix;
372
+ out += `:${pad(parts.second ?? 0, 2)}`;
373
+ if (precision === "second") return out + suffix;
374
+ out += formatFraction(parts.fraction ?? 0);
375
+ return out + suffix;
376
+ }
377
+ function toTemporal(value) {
378
+ const { parts, precision } = check(value);
379
+ if (precision === "year") {
380
+ throw new PrecisionError(
381
+ "Temporal has no year-only type, and this package will not invent a month to reach one. Use toISO() to render a year-precision value, or precisionOf() to branch before converting",
382
+ precision
383
+ );
384
+ }
385
+ const year = parts.year ?? 0;
386
+ const month = parts.month ?? 0;
387
+ if (precision === "month") {
388
+ return Temporal.PlainYearMonth.from({ year, month }, { overflow: "reject" });
389
+ }
390
+ const day = parts.day ?? 0;
391
+ if (precision === "day") {
392
+ return Temporal.PlainDate.from({ year, month, day }, { overflow: "reject" });
393
+ }
394
+ const fields = {
395
+ year,
396
+ month,
397
+ day,
398
+ hour: parts.hour ?? 0,
399
+ minute: parts.minute ?? 0,
400
+ second: parts.second ?? 0,
401
+ ...subSecondFields(parts.fraction)
402
+ };
403
+ if (parts.offsetMinutes !== void 0) {
404
+ return Temporal.ZonedDateTime.from(
405
+ { ...fields, timeZone: formatOffset(parts.offsetMinutes) },
406
+ { overflow: "reject" }
407
+ );
408
+ }
409
+ return Temporal.PlainDateTime.from(fields, { overflow: "reject" });
410
+ }
411
+ function assertKnownTimeZone(timeZone) {
412
+ try {
413
+ Temporal.Instant.fromEpochMilliseconds(0).toZonedDateTimeISO(timeZone);
414
+ } catch {
415
+ throw new RangeError(
416
+ `unknown time zone ${JSON.stringify(timeZone)}: supply an IANA identifier such as "America/New_York", or an offset such as "+05:30"`
417
+ );
418
+ }
419
+ }
420
+ function assertUsableOffset(offsetMinutes) {
421
+ if (!Number.isInteger(offsetMinutes) || offsetMinutes < -1439 || offsetMinutes > 1439) {
422
+ throw new RangeError(
423
+ `offsetMinutes must be an integer in -1439-1439 (signed minutes from UTC); received ${String(offsetMinutes)}`
424
+ );
425
+ }
426
+ }
427
+ function resolveTimeZone(parts, options) {
428
+ if (parts.offsetMinutes !== void 0) return formatOffset(parts.offsetMinutes);
429
+ if (options?.timeZone !== void 0 && options.offsetMinutes !== void 0) {
430
+ throw new TypeError(
431
+ "supply either timeZone or offsetMinutes, not both: two answers to one question is a guess waiting to happen"
432
+ );
433
+ }
434
+ if (options?.offsetMinutes !== void 0) {
435
+ assertUsableOffset(options.offsetMinutes);
436
+ return formatOffset(options.offsetMinutes);
437
+ }
438
+ if (options?.timeZone !== void 0) {
439
+ assertKnownTimeZone(options.timeZone);
440
+ return options.timeZone;
441
+ }
442
+ throw new MissingZoneError(
443
+ "this value carries no offsetMinutes, and no zone was supplied: an absolute instant needs a zone, and this package supplies none. Pass { timeZone } or { offsetMinutes }. It will not fall back to the host time zone, to UTC, or to any other default, because a fallback turns an unknown offset into a plausible wrong instant"
444
+ );
445
+ }
446
+ function toZonedDateTime(value, options) {
447
+ const { parts, precision } = check(value);
448
+ if (!atLeast(precision, "hour")) {
449
+ const alsoNoZone = parts.offsetMinutes === void 0 && options?.timeZone === void 0 && options?.offsetMinutes === void 0;
450
+ throw new PrecisionError(
451
+ `an absolute instant needs a time of day, and this value has ${precision} precision. Supplying one would mean inventing components the value does not carry. Use toTemporal() for the calendar value${alsoNoZone ? ". The zone is missing too: this value carries no offsetMinutes and no { timeZone } or { offsetMinutes } was supplied, and neither the host time zone nor UTC will be used in their place" : ""}`,
452
+ precision
453
+ );
454
+ }
455
+ const timeZone = resolveTimeZone(parts, options);
456
+ const local = Temporal.PlainDateTime.from(
457
+ {
458
+ year: parts.year ?? 0,
459
+ month: parts.month ?? 0,
460
+ day: parts.day ?? 0,
461
+ hour: parts.hour ?? 0,
462
+ minute: parts.minute ?? 0,
463
+ second: parts.second ?? 0,
464
+ ...subSecondFields(parts.fraction)
465
+ },
466
+ { overflow: "reject" }
467
+ );
468
+ const earliest = local.toZonedDateTime(timeZone, { disambiguation: "earlier" });
469
+ const latest = local.toZonedDateTime(timeZone, { disambiguation: "later" });
470
+ if (!earliest.equals(latest)) {
471
+ const skipped = !earliest.toPlainDateTime().equals(local);
472
+ throw new AmbiguousLocalTimeError(
473
+ skipped ? `${local.toString()} does not exist in ${timeZone}: a daylight-saving transition skips it, so there is no instant to return. Say which side of the transition you mean by passing that offset as { offsetMinutes }` : `${local.toString()} happens twice in ${timeZone}: a daylight-saving transition repeats it, at ${earliest.offset} and again at ${latest.offset}, so there is no single instant to return. Choose the one you mean and pass it as { offsetMinutes }`
474
+ );
475
+ }
476
+ return earliest;
477
+ }
478
+ function toInstant(value, options) {
479
+ return toZonedDateTime(value, options).toInstant();
480
+ }
481
+
482
+ export { AmbiguousLocalTimeError, DatePartsError, MissingZoneError, PRECISION_LADDER, PrecisionError, assertValidParts, isValidParts, precisionOf, toISO, toInstant, toTemporal, toZonedDateTime, validateParts };
483
+ //# sourceMappingURL=index.js.map
484
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/errors.ts","../src/fraction.ts","../src/validate.ts","../src/convert.ts"],"names":["nanosecondsOf"],"mappings":";;;;AAsCO,IAAM,gBAAA,GAAmB;AAAA,EAC9B,MAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF;;;ACrCO,IAAM,cAAA,GAAN,cAA6B,KAAA,CAAM;AAAA,EAC/B,MAAA;AAAA,EAET,YAAY,MAAA,EAAoC;AAC9C,IAAA,KAAA,CAAM,CAAA,oBAAA,EAAuB,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,KAAU,KAAA,CAAM,OAAO,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAC9E,IAAA,IAAA,CAAK,IAAA,GAAO,gBAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AACF;AASO,IAAM,gBAAA,GAAN,cAA+B,KAAA,CAAM;AAAA,EAC1C,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,kBAAA;AAAA,EACd;AACF;AAMO,IAAM,cAAA,GAAN,cAA6B,KAAA,CAAM;AAAA;AAAA,EAE/B,SAAA;AAAA,EAET,WAAA,CAAY,SAAiB,SAAA,EAAmB;AAC9C,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,gBAAA;AACZ,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AAAA,EACnB;AACF;AAUO,IAAM,uBAAA,GAAN,cAAsC,KAAA,CAAM;AAAA,EACjD,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,yBAAA;AAAA,EACd;AACF;;;AC3CA,IAAM,iBAAA,GAAoB,CAAA;AAG1B,IAAM,aAAA,GAAgB,iBAAA;AAGtB,IAAM,mBAAA,GAAsB,2BAAA;AAU5B,SAAS,eAAe,QAAA,EAAsC;AAC5D,EAAA,MAAM,IAAA,GAAO,OAAO,QAAQ,CAAA;AAE5B,EAAA,MAAM,KAAA,GAAQ,aAAA,CAAc,IAAA,CAAK,IAAI,CAAA;AACrC,EAAA,IAAI,KAAA,KAAU,IAAA,EAAM,OAAO,KAAA,CAAM,CAAC,CAAA,IAAK,EAAA;AAEvC,EAAA,MAAM,WAAA,GAAc,mBAAA,CAAoB,IAAA,CAAK,IAAI,CAAA;AACjD,EAAA,IAAI,WAAA,KAAgB,MAAM,OAAO,MAAA;AAEjC,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,CAAC,CAAA,IAAK,EAAA;AAC/B,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,CAAC,CAAA,IAAK,EAAA;AAC/B,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,WAAA,CAAY,CAAC,CAAC,CAAA;AACtC,EAAA,OAAO,GAAA,CAAI,MAAA,CAAO,QAAA,GAAW,CAAC,IAAI,IAAA,GAAO,IAAA;AAC3C;AAUO,SAAS,cAAc,QAAA,EAAsC;AAClE,EAAA,MAAM,MAAA,GAAS,eAAe,QAAQ,CAAA;AACtC,EAAA,IAAI,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,MAAA,GAAS,mBAAmB,OAAO,MAAA;AACtE,EAAA,OAAO,MAAA,KAAW,KAAK,CAAA,GAAI,MAAA,CAAO,OAAO,MAAA,CAAO,iBAAA,EAAmB,GAAG,CAAC,CAAA;AACzE;AAcO,SAAS,aAAa,QAAA,EAAmC;AAC9D,EAAA,IAAI,CAAC,OAAO,QAAA,CAAS,QAAQ,KAAK,QAAA,GAAW,CAAA,IAAK,YAAY,CAAA,EAAG;AAC/D,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,KAAA,EAAO;AAAA,QACL,SAAA,EAAW,UAAA;AAAA,QACX,IAAA,EAAM,cAAA;AAAA,QACN,OAAA,EAAS,CAAA,0FAAA,EAA6F,MAAA,CAAO,QAAQ,CAAC,CAAA;AAAA;AACxH,KACF;AAAA,EACF;AAEA,EAAA,MAAM,WAAA,GAAc,cAAc,QAAQ,CAAA;AAC1C,EAAA,IAAI,gBAAgB,MAAA,EAAW;AAC7B,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,KAAA,EAAO;AAAA,QACL,SAAA,EAAW,UAAA;AAAA,QACX,IAAA,EAAM,uBAAA;AAAA,QACN,OAAA,EAAS,CAAA,+GAAA,EAAkH,MAAA,CAAO,QAAQ,CAAC,CAAA,iGAAA;AAAA;AAC7I,KACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,WAAA,EAAY;AACjC;;;ACxFA,IAAM,SAAA,GAAiC,CAAC,GAAG,gBAAA,EAAkB,eAAe,CAAA;AAI5E,IAAM,cAAA,GAA2E;AAAA,EAC/E,IAAA,EAAM,CAAC,GAAA,EAAM,IAAI,CAAA;AAAA,EACjB,KAAA,EAAO,CAAC,CAAA,EAAG,EAAE,CAAA;AAAA,EACb,GAAA,EAAK,CAAC,CAAA,EAAG,EAAE,CAAA;AAAA,EACX,IAAA,EAAM,CAAC,CAAA,EAAG,EAAE,CAAA;AAAA,EACZ,MAAA,EAAQ,CAAC,CAAA,EAAG,EAAE,CAAA;AAAA,EACd,MAAA,EAAQ,CAAC,CAAA,EAAG,EAAE,CAAA;AAAA,EACd,aAAA,EAAe,CAAC,KAAA,EAAO,IAAI;AAC7B,CAAA;AAEA,IAAM,WAAA,GAA8D;AAAA,EAClE,KAAA,EAAO,qCAAA;AAAA,EACP,aAAA,EAAe;AACjB,CAAA;AAEA,IAAM,WAAA,GAAc;AAAA,EAClB,SAAA;AAAA,EACA,UAAA;AAAA,EACA,OAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,WAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA;AAEA,IAAM,aAAA,GAAgB,CAAC,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,IAAI,EAAE,CAAA;AAGrE,SAAS,WAAW,IAAA,EAAuB;AACzC,EAAA,OAAO,OAAO,CAAA,KAAM,CAAA,KAAM,OAAO,GAAA,KAAQ,CAAA,IAAK,OAAO,GAAA,KAAQ,CAAA,CAAA;AAC/D;AAEA,SAAS,WAAA,CAAY,MAAc,KAAA,EAAuB;AACxD,EAAA,IAAI,UAAU,CAAA,EAAG,OAAO,UAAA,CAAW,IAAI,IAAI,EAAA,GAAK,EAAA;AAChD,EAAA,OAAO,aAAA,CAAc,KAAA,GAAQ,CAAC,CAAA,IAAK,CAAA;AACrC;AAEA,SAAS,UAAU,KAAA,EAAuB;AACxC,EAAA,OAAO,YAAY,KAAA,GAAQ,CAAC,KAAK,CAAA,MAAA,EAAS,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AACzD;AAGA,SAAS,SAAS,KAAA,EAAwB;AACxC,EAAA,IAAI,KAAA,KAAU,MAAM,OAAO,MAAA;AAC3B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,WAAA;AAChC,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,IAAI,MAAA,CAAO,KAAA,CAAM,KAAK,CAAA,EAAG,OAAO,KAAA;AAChC,IAAA,IAAI,CAAC,OAAO,QAAA,CAAS,KAAK,GAAG,OAAO,KAAA,GAAQ,IAAI,UAAA,GAAa,WAAA;AAC7D,IAAA,OAAO,CAAA,WAAA,EAAc,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAAA,EACpC;AACA,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,UAAA;AACtC,EAAA,IAAI,OAAO,KAAA,KAAU,SAAA,SAAkB,CAAA,YAAA,EAAe,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AACnE,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,UAAA;AACtC,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,UAAA;AACtC,EAAA,IAAI,OAAO,KAAA,KAAU,UAAA,EAAY,OAAO,YAAA;AACxC,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,UAAA;AACjC,EAAA,OAAO,WAAA;AACT;AAEA,SAAS,QAAQ,MAAA,EAAwD;AACvE,EAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,MAAA,EAAO;AAChC;AAUA,SAAS,aAAA,CACP,QACA,IAAA,EAG0D;AAC1D,EAAA,IAAI;AACF,IAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,GAAA,EAAK,MAAA,CAAO,IAAI,CAAA,EAAE;AAAA,EACvC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,KAAA,EAAO;AAAA,QACL,SAAA,EAAW,IAAA;AAAA,QACX,IAAA,EAAM,YAAA;AAAA,QACN,OAAA,EAAS,GAAG,IAAI,CAAA,2HAAA;AAAA;AAClB,KACF;AAAA,EACF;AACF;AAkBO,SAAS,cAAc,KAAA,EAAkC;AAC9D,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,IAAA,EAAM;AAC/C,IAAA,OAAO,OAAA,CAAQ;AAAA,MACb;AAAA,QACE,SAAA,EAAW,OAAA;AAAA,QACX,IAAA,EAAM,oBAAA;AAAA,QACN,OAAA,EAAS,CAAA,0FAAA,EAA6F,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA;AACvH,KACD,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,iBAAiB,IAAA,EAAM;AACzB,IAAA,OAAO,OAAA,CAAQ;AAAA,MACb;AAAA,QACE,SAAA,EAAW,OAAA;AAAA,QACX,IAAA,EAAM,oBAAA;AAAA,QACN,OAAA,EACE;AAAA;AACJ,KACD,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,IAAA,OAAO,OAAA,CAAQ;AAAA,MACb;AAAA,QACE,SAAA,EAAW,OAAA;AAAA,QACX,IAAA,EAAM,oBAAA;AAAA,QACN,OAAA,EACE;AAAA;AACJ,KACD,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,MAAA,GAAS,KAAA;AACf,EAAA,MAAM,SAA4B,EAAC;AACnC,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAc;AAClC,EAAA,MAAM,SAA4C,EAAC;AAEnD,EAAA,KAAA,MAAW,QAAQ,SAAA,EAAW;AAC5B,IAAA,MAAM,IAAA,GAAO,aAAA,CAAc,MAAA,EAAQ,IAAI,CAAA;AACvC,IAAA,IAAI,CAAC,KAAK,EAAA,EAAI;AACZ,MAAA,OAAA,CAAQ,IAAI,IAAI,CAAA;AAChB,MAAA,MAAA,CAAO,IAAA,CAAK,KAAK,KAAK,CAAA;AACtB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AAEjB,IAAA,IAAI,QAAQ,MAAA,EAAW;AACvB,IAAA,OAAA,CAAQ,IAAI,IAAI,CAAA;AAEhB,IAAA,IAAI,OAAO,QAAQ,QAAA,EAAU;AAC3B,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,SAAA,EAAW,IAAA;AAAA,QACX,IAAA,EAAM,cAAA;AAAA,QACN,SAAS,CAAA,EAAG,IAAI,CAAA,4BAAA,EAA+B,QAAA,CAAS,GAAG,CAAC,CAAA;AAAA,OAC7D,CAAA;AACD,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,GAAG,CAAA,EAAG;AACzB,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,SAAA,EAAW,IAAA;AAAA,QACX,IAAA,EAAM,YAAA;AAAA,QACN,SAAS,CAAA,EAAG,IAAI,CAAA,mCAAA,EAAsC,QAAA,CAAS,GAAG,CAAC,CAAA;AAAA,OACpE,CAAA;AACD,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,SAAS,UAAA,EAAY;AACvB,MAAA,MAAM,OAAA,GAAU,aAAa,GAAG,CAAA;AAChC,MAAA,IAAI,OAAA,CAAQ,EAAA,EAAI,MAAA,CAAO,QAAA,GAAW,GAAA;AAAA,WAC7B,MAAA,CAAO,IAAA,CAAK,OAAA,CAAQ,KAAK,CAAA;AAC9B,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,GAAG,CAAA,EAAG;AAC1B,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,SAAA,EAAW,IAAA;AAAA,QACX,IAAA,EAAM,gBAAA;AAAA,QACN,SAAS,CAAA,EAAG,IAAI,CAAA,6CAAA,EAAgD,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,OAC5E,CAAA;AACD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,CAAC,GAAA,EAAK,IAAI,CAAA,GAAI,eAAe,IAAI,CAAA;AACvC,IAAA,IAAI,GAAA,GAAM,GAAA,IAAO,GAAA,GAAM,IAAA,EAAM;AAC3B,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,SAAS,MAAA,GACL;AAAA,UACE,SAAA,EAAW,MAAA;AAAA,UACX,IAAA,EAAM,sBAAA;AAAA,UACN,OAAA,EAAS,CAAA,kCAAA,EAAqC,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA,EAAI,MAAA,CAAO,IAAI,CAAC,CAAA,sBAAA,EAAyB,MAAA,CAAO,GAAG,CAAC,CAAA,8EAAA;AAAA,SAC/G,GACA;AAAA,UACE,SAAA,EAAW,IAAA;AAAA,UACX,IAAA,EAAM,cAAA;AAAA,UACN,SAAS,CAAA,EAAG,IAAI,0BAA0B,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA,EAAI,MAAA,CAAO,IAAI,CAAC,CAAA,EAAG,YAAY,IAAI,CAAA,IAAK,EAAE,CAAA,sBAAA,EAAyB,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA;AACrI,OACN;AACA,MAAA;AAAA,IACF;AAEA,IAAA,MAAA,CAAO,IAAI,CAAA,GAAI,GAAA;AAAA,EACjB;AAEA,EAAA,eAAA,CAAgB,SAAS,MAAM,CAAA;AAC/B,EAAA,uBAAA,CAAwB,SAAS,MAAM,CAAA;AACvC,EAAA,iBAAA,CAAkB,QAAQ,MAAM,CAAA;AAEhC,EAAA,IAAI,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG,OAAO,QAAQ,MAAM,CAAA;AAE5C,EAAA,MAAM,QAAuD,EAAC;AAC9D,EAAA,KAAA,MAAW,QAAQ,SAAA,EAAW;AAC5B,IAAA,MAAM,IAAA,GAAO,OAAO,IAAI,CAAA;AACxB,IAAA,IAAI,IAAA,KAAS,MAAA,EAAW,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAAA,EACxC;AAEA,EAAA,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,OAAO,SAAA,EAAW,cAAA,CAAe,OAAO,CAAA,EAAe;AAC/E;AAEA,SAAS,eAAe,OAAA,EAAuD;AAC7E,EAAA,IAAI,OAAA;AACJ,EAAA,KAAA,MAAW,QAAQ,gBAAA,EAAkB;AACnC,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA,EAAG,OAAA,GAAU,IAAA;AAAA,EACnC;AACA,EAAA,OAAO,OAAA;AACT;AAEA,SAAS,eAAA,CAAgB,SAAgC,MAAA,EAAiC;AACxF,EAAA,MAAM,OAAA,GAAU,eAAe,OAAO,CAAA;AAEtC,EAAA,IAAI,YAAY,MAAA,EAAW;AACzB,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,SAAA,EAAW,MAAA;AAAA,MACX,IAAA,EAAM,mBAAA;AAAA,MACN,OAAA,EACE;AAAA,KACH,CAAA;AACD,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,YAAA,GAAe,gBAAA,CAAiB,OAAA,CAAQ,OAAO,CAAA;AACrD,EAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,YAAA,EAAc,SAAS,CAAA,EAAG;AACpD,IAAA,MAAM,IAAA,GAAO,iBAAiB,KAAK,CAAA;AACnC,IAAA,IAAI,IAAA,KAAS,MAAA,IAAa,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA,EAAG;AAC7C,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,SAAA,EAAW,IAAA;AAAA,MACX,IAAA,EAAM,mBAAA;AAAA,MACN,OAAA,EAAS,CAAA,EAAG,IAAI,CAAA,kBAAA,EAAqB,OAAO,CAAA,+GAAA;AAAA,KAC7C,CAAA;AAAA,EACH;AACF;AAEA,SAAS,uBAAA,CAAwB,SAAgC,MAAA,EAAiC;AAChG,EAAA,IAAI,CAAC,QAAQ,GAAA,CAAI,eAAe,KAAK,OAAA,CAAQ,GAAA,CAAI,MAAM,CAAA,EAAG;AAC1D,EAAA,MAAA,CAAO,IAAA,CAAK;AAAA,IACV,SAAA,EAAW,eAAA;AAAA,IACX,IAAA,EAAM,4BAAA;AAAA,IACN,OAAA,EACE;AAAA,GACH,CAAA;AACH;AAEA,SAAS,iBAAA,CACP,QACA,MAAA,EACM;AACN,EAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAO,GAAA,EAAI,GAAI,MAAA;AAC7B,EAAA,IAAI,IAAA,KAAS,MAAA,IAAa,KAAA,KAAU,MAAA,IAAa,QAAQ,MAAA,EAAW;AAEpE,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,IAAA,EAAM,KAAK,CAAA;AACrC,EAAA,IAAI,OAAO,KAAA,EAAO;AAElB,EAAA,MAAM,QAAA,GACJ,KAAA,KAAU,CAAA,GACN,CAAA,UAAA,EAAa,MAAA,CAAO,IAAI,CAAC,CAAA,IAAA,EAAO,UAAA,CAAW,IAAI,CAAA,GAAI,GAAA,GAAM,OAAO,CAAA,8CAAA,CAAA,GAChE,EAAA;AACN,EAAA,MAAA,CAAO,IAAA,CAAK;AAAA,IACV,SAAA,EAAW,KAAA;AAAA,IACX,IAAA,EAAM,uBAAA;AAAA,IACN,SAAS,CAAA,IAAA,EAAO,MAAA,CAAO,GAAG,CAAC,CAAA,iBAAA,EAAoB,UAAU,KAAK,CAAC,CAAA,CAAA,EAAI,MAAA,CAAO,IAAI,CAAC,CAAA,KAAA,EAAQ,OAAO,KAAK,CAAC,QAAQ,QAAQ,CAAA;AAAA,GACrH,CAAA;AACH;AAGO,SAAS,aAAa,KAAA,EAAoC;AAC/D,EAAA,OAAO,aAAA,CAAc,KAAK,CAAA,CAAE,KAAA;AAC9B;AAMO,SAAS,iBAAiB,KAAA,EAA2B;AAC1D,EAAA,MAAM,MAAA,GAAS,cAAc,KAAK,CAAA;AAClC,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,QAAa,IAAI,cAAA,CAAe,OAAO,MAAM,CAAA;AACzD,EAAA,OAAO,MAAA,CAAO,KAAA;AAChB;AAQO,SAAS,YAAY,KAAA,EAA2B;AACrD,EAAA,MAAM,MAAA,GAAS,cAAc,KAAK,CAAA;AAClC,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,QAAa,IAAI,cAAA,CAAe,OAAO,MAAM,CAAA;AACzD,EAAA,OAAO,MAAA,CAAO,SAAA;AAChB;AC5TA,SAAS,MAAM,KAAA,EAAyB;AACtC,EAAA,MAAM,MAAA,GAAS,cAAc,KAAK,CAAA;AAClC,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,QAAa,IAAI,cAAA,CAAe,OAAO,MAAM,CAAA;AACzD,EAAA,OAAO,EAAE,KAAA,EAAO,MAAA,CAAO,KAAA,EAAO,SAAA,EAAW,OAAO,SAAA,EAAU;AAC5D;AAEA,SAAS,OAAA,CAAQ,WAAsB,KAAA,EAA2B;AAChE,EAAA,OAAO,iBAAiB,OAAA,CAAQ,SAAS,CAAA,IAAK,gBAAA,CAAiB,QAAQ,KAAK,CAAA;AAC9E;AAEA,SAAS,GAAA,CAAI,OAAe,KAAA,EAAuB;AACjD,EAAA,OAAO,MAAA,CAAO,KAAK,CAAA,CAAE,QAAA,CAAS,OAAO,GAAG,CAAA;AAC1C;AASA,SAAS,aAAa,aAAA,EAA+B;AACnD,EAAA,MAAM,IAAA,GAAO,aAAA,GAAgB,CAAA,GAAI,GAAA,GAAM,GAAA;AACvC,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,aAAa,CAAA;AACvC,EAAA,OAAO,GAAG,IAAI,CAAA,EAAG,GAAA,CAAI,IAAA,CAAK,MAAM,QAAA,GAAW,EAAE,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA,EAAI,GAAA,CAAI,QAAA,GAAW,EAAA,EAAI,CAAC,CAAC,CAAA,CAAA;AAC7E;AAaA,SAASA,eAAc,QAAA,EAA0B;AAC/C,EAAA,MAAM,OAAA,GAAU,aAAa,QAAQ,CAAA;AACrC,EAAA,IAAI,CAAC,QAAQ,EAAA,EAAI,MAAM,IAAI,cAAA,CAAe,CAAC,OAAA,CAAQ,KAAK,CAAC,CAAA;AACzD,EAAA,OAAO,OAAA,CAAQ,WAAA;AACjB;AAGA,SAAS,eAAe,QAAA,EAA0B;AAChD,EAAA,MAAM,MAAA,GAAS,IAAIA,cAAAA,CAAc,QAAQ,GAAG,CAAC,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAChE,EAAA,OAAO,CAAA,CAAA,EAAI,MAAA,KAAW,EAAA,GAAK,GAAA,GAAM,MAAM,CAAA,CAAA;AACzC;AAEA,SAAS,gBAAgB,QAAA,EAIvB;AACA,EAAA,IAAI,QAAA,KAAa,MAAA,EAAW,OAAO,EAAC;AACpC,EAAA,MAAM,WAAA,GAAcA,eAAc,QAAQ,CAAA;AAC1C,EAAA,OAAO;AAAA,IACL,WAAA,EAAa,IAAA,CAAK,KAAA,CAAM,WAAA,GAAc,GAAG,CAAA;AAAA,IACzC,WAAA,EAAa,IAAA,CAAK,KAAA,CAAO,WAAA,GAAc,MAAO,GAAG,CAAA;AAAA,IACjD,YAAY,WAAA,GAAc;AAAA,GAC5B;AACF;AAcO,SAAS,MAAM,KAAA,EAAwB;AAC5C,EAAA,MAAM,EAAE,KAAA,EAAO,SAAA,EAAU,GAAI,MAAM,KAAK,CAAA;AACxC,EAAA,MAAM,SAAS,KAAA,CAAM,aAAA,KAAkB,SAAY,EAAA,GAAK,YAAA,CAAa,MAAM,aAAa,CAAA;AAExF,EAAA,IAAI,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,IAAA,IAAQ,GAAG,CAAC,CAAA;AAChC,EAAA,IAAI,SAAA,KAAc,QAAQ,OAAO,GAAA;AAEjC,EAAA,GAAA,IAAO,IAAI,GAAA,CAAI,KAAA,CAAM,KAAA,IAAS,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AACnC,EAAA,IAAI,SAAA,KAAc,SAAS,OAAO,GAAA;AAElC,EAAA,GAAA,IAAO,IAAI,GAAA,CAAI,KAAA,CAAM,GAAA,IAAO,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AACjC,EAAA,IAAI,SAAA,KAAc,OAAO,OAAO,GAAA;AAEhC,EAAA,GAAA,IAAO,IAAI,GAAA,CAAI,KAAA,CAAM,IAAA,IAAQ,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AAClC,EAAA,IAAI,SAAA,KAAc,MAAA,EAAQ,OAAO,GAAA,GAAM,MAAA;AAEvC,EAAA,GAAA,IAAO,IAAI,GAAA,CAAI,KAAA,CAAM,MAAA,IAAU,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AACpC,EAAA,IAAI,SAAA,KAAc,QAAA,EAAU,OAAO,GAAA,GAAM,MAAA;AAEzC,EAAA,GAAA,IAAO,IAAI,GAAA,CAAI,KAAA,CAAM,MAAA,IAAU,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AACpC,EAAA,IAAI,SAAA,KAAc,QAAA,EAAU,OAAO,GAAA,GAAM,MAAA;AAEzC,EAAA,GAAA,IAAO,cAAA,CAAe,KAAA,CAAM,QAAA,IAAY,CAAC,CAAA;AACzC,EAAA,OAAO,GAAA,GAAM,MAAA;AACf;AAyBO,SAAS,WAAW,KAAA,EAA+B;AACxD,EAAA,MAAM,EAAE,KAAA,EAAO,SAAA,EAAU,GAAI,MAAM,KAAK,CAAA;AAExC,EAAA,IAAI,cAAc,MAAA,EAAQ;AACxB,IAAA,MAAM,IAAI,cAAA;AAAA,MACR,mLAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,MAAM,IAAA,IAAQ,CAAA;AAC3B,EAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,IAAS,CAAA;AAE7B,EAAA,IAAI,cAAc,OAAA,EAAS;AACzB,IAAA,OAAO,QAAA,CAAS,cAAA,CAAe,IAAA,CAAK,EAAE,IAAA,EAAM,OAAM,EAAG,EAAE,QAAA,EAAU,QAAA,EAAU,CAAA;AAAA,EAC7E;AAEA,EAAA,MAAM,GAAA,GAAM,MAAM,GAAA,IAAO,CAAA;AACzB,EAAA,IAAI,cAAc,KAAA,EAAO;AACvB,IAAA,OAAO,QAAA,CAAS,SAAA,CAAU,IAAA,CAAK,EAAE,IAAA,EAAM,KAAA,EAAO,GAAA,EAAI,EAAG,EAAE,QAAA,EAAU,QAAA,EAAU,CAAA;AAAA,EAC7E;AAEA,EAAA,MAAM,MAAA,GAAS;AAAA,IACb,IAAA;AAAA,IACA,KAAA;AAAA,IACA,GAAA;AAAA,IACA,IAAA,EAAM,MAAM,IAAA,IAAQ,CAAA;AAAA,IACpB,MAAA,EAAQ,MAAM,MAAA,IAAU,CAAA;AAAA,IACxB,MAAA,EAAQ,MAAM,MAAA,IAAU,CAAA;AAAA,IACxB,GAAG,eAAA,CAAgB,KAAA,CAAM,QAAQ;AAAA,GACnC;AAEA,EAAA,IAAI,KAAA,CAAM,kBAAkB,MAAA,EAAW;AACrC,IAAA,OAAO,SAAS,aAAA,CAAc,IAAA;AAAA,MAC5B,EAAE,GAAG,MAAA,EAAQ,UAAU,YAAA,CAAa,KAAA,CAAM,aAAa,CAAA,EAAE;AAAA,MACzD,EAAE,UAAU,QAAA;AAAS,KACvB;AAAA,EACF;AAEA,EAAA,OAAO,SAAS,aAAA,CAAc,IAAA,CAAK,QAAQ,EAAE,QAAA,EAAU,UAAU,CAAA;AACnE;AAEA,SAAS,oBAAoB,QAAA,EAAwB;AACnD,EAAA,IAAI;AAEF,IAAA,QAAA,CAAS,OAAA,CAAQ,qBAAA,CAAsB,CAAC,CAAA,CAAE,mBAAmB,QAAQ,CAAA;AAAA,EACvE,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,UAAA;AAAA,MACR,CAAA,kBAAA,EAAqB,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAC,CAAA,qFAAA;AAAA,KAC/C;AAAA,EACF;AACF;AAEA,SAAS,mBAAmB,aAAA,EAA6B;AACvD,EAAA,IAAI,CAAC,OAAO,SAAA,CAAU,aAAa,KAAK,aAAA,GAAgB,KAAA,IAAS,gBAAgB,IAAA,EAAM;AACrF,IAAA,MAAM,IAAI,UAAA;AAAA,MACR,CAAA,mFAAA,EAAsF,MAAA,CAAO,aAAa,CAAC,CAAA;AAAA,KAC7G;AAAA,EACF;AACF;AAUA,SAAS,eAAA,CAAgB,OAAkB,OAAA,EAA0C;AACnF,EAAA,IAAI,MAAM,aAAA,KAAkB,MAAA,EAAW,OAAO,YAAA,CAAa,MAAM,aAAa,CAAA;AAE9E,EAAA,IAAI,OAAA,EAAS,QAAA,KAAa,MAAA,IAAa,OAAA,CAAQ,kBAAkB,MAAA,EAAW;AAC1E,IAAA,MAAM,IAAI,SAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAEA,EAAA,IAAI,OAAA,EAAS,kBAAkB,MAAA,EAAW;AACxC,IAAA,kBAAA,CAAmB,QAAQ,aAAa,CAAA;AACxC,IAAA,OAAO,YAAA,CAAa,QAAQ,aAAa,CAAA;AAAA,EAC3C;AAEA,EAAA,IAAI,OAAA,EAAS,aAAa,MAAA,EAAW;AACnC,IAAA,mBAAA,CAAoB,QAAQ,QAAQ,CAAA;AACpC,IAAA,OAAO,OAAA,CAAQ,QAAA;AAAA,EACjB;AAEA,EAAA,MAAM,IAAI,gBAAA;AAAA,IACR;AAAA,GACF;AACF;AAuBO,SAAS,eAAA,CAAgB,OAAgB,OAAA,EAA+C;AAC7F,EAAA,MAAM,EAAE,KAAA,EAAO,SAAA,EAAU,GAAI,MAAM,KAAK,CAAA;AAExC,EAAA,IAAI,CAAC,OAAA,CAAQ,SAAA,EAAW,MAAM,CAAA,EAAG;AAM/B,IAAA,MAAM,UAAA,GACJ,MAAM,aAAA,KAAkB,MAAA,IACxB,SAAS,QAAA,KAAa,MAAA,IACtB,SAAS,aAAA,KAAkB,MAAA;AAE7B,IAAA,MAAM,IAAI,cAAA;AAAA,MACR,CAAA,4DAAA,EAA+D,SAAS,CAAA,2HAAA,EACtE,UAAA,GACI,6LACA,EACN,CAAA,CAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,QAAA,GAAW,eAAA,CAAgB,KAAA,EAAO,OAAO,CAAA;AAC/C,EAAA,MAAM,KAAA,GAAQ,SAAS,aAAA,CAAc,IAAA;AAAA,IACnC;AAAA,MACE,IAAA,EAAM,MAAM,IAAA,IAAQ,CAAA;AAAA,MACpB,KAAA,EAAO,MAAM,KAAA,IAAS,CAAA;AAAA,MACtB,GAAA,EAAK,MAAM,GAAA,IAAO,CAAA;AAAA,MAClB,IAAA,EAAM,MAAM,IAAA,IAAQ,CAAA;AAAA,MACpB,MAAA,EAAQ,MAAM,MAAA,IAAU,CAAA;AAAA,MACxB,MAAA,EAAQ,MAAM,MAAA,IAAU,CAAA;AAAA,MACxB,GAAG,eAAA,CAAgB,KAAA,CAAM,QAAQ;AAAA,KACnC;AAAA,IACA,EAAE,UAAU,QAAA;AAAS,GACvB;AAOA,EAAA,MAAM,WAAW,KAAA,CAAM,eAAA,CAAgB,UAAU,EAAE,cAAA,EAAgB,WAAW,CAAA;AAC9E,EAAA,MAAM,SAAS,KAAA,CAAM,eAAA,CAAgB,UAAU,EAAE,cAAA,EAAgB,SAAS,CAAA;AAE1E,EAAA,IAAI,CAAC,QAAA,CAAS,MAAA,CAAO,MAAM,CAAA,EAAG;AAG5B,IAAA,MAAM,UAAU,CAAC,QAAA,CAAS,eAAA,EAAgB,CAAE,OAAO,KAAK,CAAA;AACxD,IAAA,MAAM,IAAI,uBAAA;AAAA,MACR,UACI,CAAA,EAAG,KAAA,CAAM,UAAU,CAAA,mBAAA,EAAsB,QAAQ,CAAA,gKAAA,CAAA,GACjD,CAAA,EAAG,MAAM,QAAA,EAAU,qBAAqB,QAAQ,CAAA,8CAAA,EAAiD,SAAS,MAAM,CAAA,cAAA,EAAiB,OAAO,MAAM,CAAA,mGAAA;AAAA,KACpJ;AAAA,EACF;AAEA,EAAA,OAAO,QAAA;AACT;AAQO,SAAS,SAAA,CAAU,OAAgB,OAAA,EAAyC;AACjF,EAAA,OAAO,eAAA,CAAgB,KAAA,EAAO,OAAO,CAAA,CAAE,SAAA,EAAU;AACnD","file":"index.js","sourcesContent":["/**\n * The date-parts value every `@cosyte/*` parser hands back, and the only shape\n * this package accepts.\n *\n * Two rules make it a contract rather than a bag of numbers.\n *\n * 1. CONTIGUITY. Components are present from the most significant downward with\n * no gaps. `{ year, month }` is legal; `{ year, day }` is not.\n * 2. PRECISION IS WHAT IS PRESENT. The least significant present component IS\n * the precision of the value. Nothing here adds a component that was not\n * supplied, and an absent `offsetMinutes` means the offset is UNKNOWN, which\n * is not the same as UTC and not the same as the host zone.\n *\n * `month` is SPEC-NATIVE 1-12, not JavaScript's 0-11.\n */\nexport interface DateParts {\n /** Four digits, 1000-9999. Required. Two-digit years are never interpreted. */\n readonly year?: number;\n /** 1-12, spec-native. January is 1. */\n readonly month?: number;\n /** 1-31, and a day that exists in the given month of the given year. */\n readonly day?: number;\n /** 0-23. */\n readonly hour?: number;\n /** 0-59. */\n readonly minute?: number;\n /** 0-59. Leap seconds are not representable. */\n readonly second?: number;\n /** Sub-second precision as a fraction of one second: `0 <= fraction < 1`. */\n readonly fraction?: number;\n /**\n * Signed minutes from UTC, -1439 to 1439. ABSENT means the offset is unknown.\n * It never means zero, and it is never filled in from the host zone.\n */\n readonly offsetMinutes?: number;\n}\n\n/** The precision ladder, most significant first. */\nexport const PRECISION_LADDER = [\n 'year',\n 'month',\n 'day',\n 'hour',\n 'minute',\n 'second',\n 'fraction',\n] as const;\n\n/** The least significant component present in a value IS its precision. */\nexport type Precision = (typeof PRECISION_LADDER)[number];\n\n/** Every component name the contract defines. */\nexport type PartName = Precision | 'offsetMinutes';\n\n/** What a diagnostic says went wrong. */\nexport type IssueCode =\n /** The argument is not a date-parts object at all. */\n | 'not-a-parts-object'\n /** A more significant component is absent while a less significant one is present. */\n | 'missing-component'\n /** Reading the component threw, so its value could not be seen at all. */\n | 'unreadable'\n /** The component is present but is not a number. */\n | 'not-a-number'\n /** The component is a number but is `NaN` or infinite. */\n | 'not-finite'\n /** The component must be a whole number and is not. */\n | 'not-an-integer'\n /** The component is outside the range the contract gives it. */\n | 'out-of-range'\n /** The year is not four digits. No century window is ever applied. */\n | 'year-not-four-digits'\n /** The sub-second value carries detail below nanosecond resolution. */\n | 'finer-than-nanosecond'\n /** An offset was given for a value that states no time of day. */\n | 'offset-without-time-of-day'\n /** The components are each in range but together denote no real date. */\n | 'no-such-calendar-date';\n\n/** One thing wrong with one component, named. */\nexport interface ValidationIssue {\n /** The component at fault, or `'value'` when the argument itself is wrong. */\n readonly component: PartName | 'value';\n readonly code: IssueCode;\n /** Human-readable, names the component and the range or calendar rule broken. */\n readonly message: string;\n}\n\n/** A validated value, carrying only the recognised components that were present. */\nexport interface ValidPartsResult {\n readonly valid: true;\n readonly parts: DateParts;\n readonly precision: Precision;\n}\n\n/** An invalid value, with every fault found rather than only the first. */\nexport interface InvalidPartsResult {\n readonly valid: false;\n readonly issues: readonly ValidationIssue[];\n}\n\nexport type ValidationResult = ValidPartsResult | InvalidPartsResult;\n\n/**\n * How to place a value that carries no offset of its own onto the timeline.\n *\n * A value that DOES carry `offsetMinutes` is never re-zoned: its own offset is\n * what the sender said, and overriding it here would move the instant.\n */\nexport interface ZoneOptions {\n /** An IANA time zone identifier, for example `'America/New_York'`. */\n readonly timeZone?: string;\n /** A fixed offset in signed minutes from UTC, -1439 to 1439. */\n readonly offsetMinutes?: number;\n}\n","import type { ValidationIssue } from './types.js';\n\n/**\n * A conversion was asked for a value that validation reports invalid.\n *\n * `issues` is the SAME array `validateParts()` returns for that value, so a\n * caller that catches this never has to validate again to learn what was wrong,\n * and a conversion never returns a coerced or clamped stand-in.\n */\nexport class DatePartsError extends Error {\n readonly issues: readonly ValidationIssue[];\n\n constructor(issues: readonly ValidationIssue[]) {\n super(`invalid date parts: ${issues.map((issue) => issue.message).join('; ')}`);\n this.name = 'DatePartsError';\n this.issues = issues;\n }\n}\n\n/**\n * An absolute instant was asked for, and nothing said which zone the local time\n * is in.\n *\n * This package never falls back to the host timezone, to UTC, or to any other\n * default, because a fallback turns an unknown into a plausible wrong answer.\n */\nexport class MissingZoneError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'MissingZoneError';\n }\n}\n\n/**\n * The value does not carry enough precision for what was asked, and supplying\n * the missing components would mean inventing them.\n */\nexport class PrecisionError extends Error {\n /** The precision the value actually has. */\n readonly precision: string;\n\n constructor(message: string, precision: string) {\n super(message);\n this.name = 'PrecisionError';\n this.precision = precision;\n }\n}\n\n/**\n * A local time was placed in a named zone where it does not exist, or exists\n * twice.\n *\n * Both cases are resolved by REFUSING rather than by picking. A daylight-saving\n * transition is exactly where a silent choice shifts a clinical timeline by an\n * hour, so the caller is told and decides.\n */\nexport class AmbiguousLocalTimeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AmbiguousLocalTimeError';\n }\n}\n","import type { ValidationIssue } from './types.js';\n\n/**\n * Reading a `fraction` as whole nanoseconds, EXACTLY.\n *\n * One function answers \"how many nanoseconds is this?\" for the whole package, so\n * the validator and every converter cannot disagree about a value: a fraction the\n * validator accepts is one every converter can render, and a fraction any of them\n * refuses is refused by all of them with the same diagnostic.\n *\n * The count is read from the value's own decimal digits rather than computed as\n * `fraction * 1e9`. Binary floating point cannot hold most decimal fractions, so\n * that product misses whole nanoseconds by a hair and any tolerance wide enough\n * to forgive the hair is also wide enough to swallow a real difference. Digits\n * are exact and need no tolerance at all.\n */\n\n/** Temporal's resolution floor, and this package's: nine decimal places. */\nconst NANOSECOND_DIGITS = 9;\n\n/** `0`, or `0.` followed by the sub-second digits. */\nconst PLAIN_DECIMAL = /^0(?:\\.(\\d+))?$/;\n\n/** JavaScript prints a magnitude below `1e-6` in exponential form: `1e-9`, `1.5e-7`. */\nconst EXPONENTIAL_DECIMAL = /^(\\d)(?:\\.(\\d+))?e-(\\d+)$/;\n\n/**\n * The digits after the decimal point of the shortest decimal that round-trips to\n * `fraction`, or `undefined` if the value has no such form.\n *\n * The shortest round-tripping decimal is the value's canonical spelling: it is\n * what `0.123456789` was written as, and every longer decimal that lands on the\n * same double is the same double.\n */\nfunction fractionDigits(fraction: number): string | undefined {\n const text = String(fraction);\n\n const plain = PLAIN_DECIMAL.exec(text);\n if (plain !== null) return plain[1] ?? '';\n\n const exponential = EXPONENTIAL_DECIMAL.exec(text);\n if (exponential === null) return undefined;\n\n const lead = exponential[1] ?? '';\n const rest = exponential[2] ?? '';\n const exponent = Number(exponential[3]);\n return '0'.repeat(exponent - 1) + lead + rest;\n}\n\n/**\n * The whole nanoseconds `fraction` denotes, or `undefined` when it carries detail\n * finer than one nanosecond.\n *\n * `fraction` must already be known finite and in `[0, 1)`; {@link readFraction}\n * is the entry point that establishes both. The result is therefore always in\n * `[0, 999999999]`, never a whole second.\n */\nexport function nanosecondsOf(fraction: number): number | undefined {\n const digits = fractionDigits(fraction);\n if (digits === undefined || digits.length > NANOSECOND_DIGITS) return undefined;\n return digits === '' ? 0 : Number(digits.padEnd(NANOSECOND_DIGITS, '0'));\n}\n\n/** A fraction read as whole nanoseconds, or the diagnostic that refuses it. */\nexport type FractionReading =\n | { readonly ok: true; readonly nanoseconds: number }\n | { readonly ok: false; readonly issue: ValidationIssue };\n\n/**\n * Check a `fraction` against the contract and read it as whole nanoseconds.\n *\n * Nothing is rounded. A value carrying detail below one nanosecond is REFUSED,\n * because rounding it away would silently change what the sender said, and this\n * package exists to not do that.\n */\nexport function readFraction(fraction: number): FractionReading {\n if (!Number.isFinite(fraction) || fraction < 0 || fraction >= 1) {\n return {\n ok: false,\n issue: {\n component: 'fraction',\n code: 'out-of-range',\n message: `fraction is a share of one second and must satisfy 0 <= fraction < 1; received the number ${String(fraction)}`,\n },\n };\n }\n\n const nanoseconds = nanosecondsOf(fraction);\n if (nanoseconds === undefined) {\n return {\n ok: false,\n issue: {\n component: 'fraction',\n code: 'finer-than-nanosecond',\n message: `fraction carries detail finer than one nanosecond, which this package will not round away; received the number ${String(fraction)}. One nanosecond is nine decimal places of a second, and it is the finest value Temporal can hold`,\n },\n };\n }\n\n return { ok: true, nanoseconds };\n}\n","import { DatePartsError } from './errors.js';\nimport { readFraction } from './fraction.js';\nimport { PRECISION_LADDER } from './types.js';\nimport type {\n DateParts,\n InvalidPartsResult,\n PartName,\n Precision,\n ValidationIssue,\n ValidationResult,\n} from './types.js';\n\nconst ALL_PARTS: readonly PartName[] = [...PRECISION_LADDER, 'offsetMinutes'];\n\ntype IntegerPart = Exclude<PartName, 'fraction'>;\n\nconst INTEGER_RANGES: Readonly<Record<IntegerPart, readonly [number, number]>> = {\n year: [1000, 9999],\n month: [1, 12],\n day: [1, 31],\n hour: [0, 23],\n minute: [0, 59],\n second: [0, 59],\n offsetMinutes: [-1439, 1439],\n};\n\nconst RANGE_NOTES: Readonly<Partial<Record<IntegerPart, string>>> = {\n month: ' (spec-native, January is 1, not 0)',\n offsetMinutes: ' (signed minutes from UTC)',\n};\n\nconst MONTH_NAMES = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n] as const;\n\nconst MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as const;\n\n/** Proleptic Gregorian, applied to every year in range with no exceptions. */\nfunction isLeapYear(year: number): boolean {\n return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\n}\n\nfunction daysInMonth(year: number, month: number): number {\n if (month === 2) return isLeapYear(year) ? 29 : 28;\n return MONTH_LENGTHS[month - 1] ?? 0;\n}\n\nfunction monthName(month: number): string {\n return MONTH_NAMES[month - 1] ?? `month ${String(month)}`;\n}\n\n/** A short, non-throwing description of a value, for diagnostics. */\nfunction describe(value: unknown): string {\n if (value === null) return 'null';\n if (value === undefined) return 'undefined';\n if (typeof value === 'number') {\n if (Number.isNaN(value)) return 'NaN';\n if (!Number.isFinite(value)) return value > 0 ? 'Infinity' : '-Infinity';\n return `the number ${String(value)}`;\n }\n if (typeof value === 'string') return 'a string';\n if (typeof value === 'boolean') return `the boolean ${String(value)}`;\n if (typeof value === 'bigint') return 'a bigint';\n if (typeof value === 'symbol') return 'a symbol';\n if (typeof value === 'function') return 'a function';\n if (Array.isArray(value)) return 'an array';\n return 'an object';\n}\n\nfunction invalid(issues: readonly ValidationIssue[]): InvalidPartsResult {\n return { valid: false, issues };\n}\n\n/**\n * Read one named component off a candidate parts object.\n *\n * A component can be an accessor, and an accessor can throw. Reading it is\n * therefore guarded, so that a hostile or merely broken object is REPORTED like\n * any other fault rather than raising out of a function whose whole contract is\n * that it answers instead of throwing.\n */\nfunction readComponent(\n source: Record<string, unknown>,\n name: PartName,\n):\n | { readonly ok: true; readonly raw: unknown }\n | { readonly ok: false; readonly issue: ValidationIssue } {\n try {\n return { ok: true, raw: source[name] };\n } catch {\n return {\n ok: false,\n issue: {\n component: name,\n code: 'unreadable',\n message: `${name} could not be read: the property threw when accessed, so this package cannot see what value it holds and will not guess one`,\n },\n };\n }\n}\n\n/**\n * Check a value against the date-parts contract.\n *\n * Every fault is reported, not only the first, and every diagnostic names the\n * component at fault together with the range or calendar rule it broke. Nothing\n * is coerced, clamped or inferred: a value that does not satisfy the contract\n * comes back invalid rather than corrected.\n *\n * This function never throws, for any argument at all. Hostile input (`null`, a\n * string, a JavaScript `Date`, `NaN`, `Infinity`, a non-integer component, a\n * component that throws when it is read) is reported, not raised.\n *\n * Components are looked up by name, so an object carrying them on its prototype\n * (a class instance with accessors, which is a shape a parser may well hand\n * over) is read the same way a plain object literal is.\n */\nexport function validateParts(value: unknown): ValidationResult {\n if (typeof value !== 'object' || value === null) {\n return invalid([\n {\n component: 'value',\n code: 'not-a-parts-object',\n message: `a date-parts value must be an object carrying the components the contract names; received ${describe(value)}`,\n },\n ]);\n }\n\n if (value instanceof Date) {\n return invalid([\n {\n component: 'value',\n code: 'not-a-parts-object',\n message:\n 'a JavaScript Date is an absolute instant, not a date-parts value: it cannot express a year-only or month-only date, and it carries no offset of its own. Pass the parts the parser returned',\n },\n ]);\n }\n\n if (Array.isArray(value)) {\n return invalid([\n {\n component: 'value',\n code: 'not-a-parts-object',\n message:\n 'a date-parts value must be an object with named components; received an array, whose positions this package will not guess at',\n },\n ]);\n }\n\n const source = value as Record<string, unknown>;\n const issues: ValidationIssue[] = [];\n const present = new Set<PartName>();\n const values: Partial<Record<PartName, number>> = {};\n\n for (const name of ALL_PARTS) {\n const read = readComponent(source, name);\n if (!read.ok) {\n present.add(name);\n issues.push(read.issue);\n continue;\n }\n\n const raw = read.raw;\n // An explicit `undefined` reads as absent, the way an optional property does.\n if (raw === undefined) continue;\n present.add(name);\n\n if (typeof raw !== 'number') {\n issues.push({\n component: name,\n code: 'not-a-number',\n message: `${name} must be a number; received ${describe(raw)}`,\n });\n continue;\n }\n\n if (!Number.isFinite(raw)) {\n issues.push({\n component: name,\n code: 'not-finite',\n message: `${name} must be a finite number; received ${describe(raw)}`,\n });\n continue;\n }\n\n if (name === 'fraction') {\n const reading = readFraction(raw);\n if (reading.ok) values.fraction = raw;\n else issues.push(reading.issue);\n continue;\n }\n\n if (!Number.isInteger(raw)) {\n issues.push({\n component: name,\n code: 'not-an-integer',\n message: `${name} must be a whole number; received the number ${String(raw)}`,\n });\n continue;\n }\n\n const [low, high] = INTEGER_RANGES[name];\n if (raw < low || raw > high) {\n issues.push(\n name === 'year'\n ? {\n component: 'year',\n code: 'year-not-four-digits',\n message: `year must be a four-digit year in ${String(low)}-${String(high)}; received the number ${String(raw)}. This package applies no century window and never interprets a two-digit year`,\n }\n : {\n component: name,\n code: 'out-of-range',\n message: `${name} must be an integer in ${String(low)}-${String(high)}${RANGE_NOTES[name] ?? ''}; received the number ${String(raw)}`,\n },\n );\n continue;\n }\n\n values[name] = raw;\n }\n\n checkContiguity(present, issues);\n checkOffsetHasTimeOfDay(present, issues);\n checkCalendarDate(values, issues);\n\n if (issues.length > 0) return invalid(issues);\n\n const parts: { -readonly [K in keyof DateParts]?: number } = {};\n for (const name of ALL_PARTS) {\n const held = values[name];\n if (held !== undefined) parts[name] = held;\n }\n\n return { valid: true, parts, precision: deepestPresent(present) as Precision };\n}\n\nfunction deepestPresent(present: ReadonlySet<PartName>): Precision | undefined {\n let deepest: Precision | undefined;\n for (const name of PRECISION_LADDER) {\n if (present.has(name)) deepest = name;\n }\n return deepest;\n}\n\nfunction checkContiguity(present: ReadonlySet<PartName>, issues: ValidationIssue[]): void {\n const deepest = deepestPresent(present);\n\n if (deepest === undefined) {\n issues.push({\n component: 'year',\n code: 'missing-component',\n message:\n 'year is missing: a date-parts value must carry at least a year, and this package never infers one',\n });\n return;\n }\n\n const deepestIndex = PRECISION_LADDER.indexOf(deepest);\n for (let index = 0; index < deepestIndex; index += 1) {\n const name = PRECISION_LADDER[index];\n if (name === undefined || present.has(name)) continue;\n issues.push({\n component: name,\n code: 'missing-component',\n message: `${name} is missing while ${deepest} is present: components run from year downward with no gaps, and this package never infers the one you left out`,\n });\n }\n}\n\nfunction checkOffsetHasTimeOfDay(present: ReadonlySet<PartName>, issues: ValidationIssue[]): void {\n if (!present.has('offsetMinutes') || present.has('hour')) return;\n issues.push({\n component: 'offsetMinutes',\n code: 'offset-without-time-of-day',\n message:\n 'offsetMinutes is present but the value states no time of day: an offset qualifies a clock time, it cannot be rendered without one, and dropping it silently is not an option this package takes',\n });\n}\n\nfunction checkCalendarDate(\n values: Partial<Record<PartName, number>>,\n issues: ValidationIssue[],\n): void {\n const { year, month, day } = values;\n if (year === undefined || month === undefined || day === undefined) return;\n\n const limit = daysInMonth(year, month);\n if (day <= limit) return;\n\n const leapNote =\n month === 2\n ? `, because ${String(year)} is ${isLeapYear(year) ? 'a' : 'not a'} leap year in the proleptic Gregorian calendar`\n : '';\n issues.push({\n component: 'day',\n code: 'no-such-calendar-date',\n message: `day ${String(day)} does not exist: ${monthName(month)} ${String(year)} has ${String(limit)} days${leapNote}`,\n });\n}\n\n/** A type guard over {@link validateParts}, for callers that want a boolean. */\nexport function isValidParts(value: unknown): value is DateParts {\n return validateParts(value).valid;\n}\n\n/**\n * Validate and return the recognised components, or throw {@link DatePartsError}\n * carrying the same diagnostics {@link validateParts} would have reported.\n */\nexport function assertValidParts(value: unknown): DateParts {\n const result = validateParts(value);\n if (!result.valid) throw new DatePartsError(result.issues);\n return result.parts;\n}\n\n/**\n * The precision of a value: its least significant present component.\n *\n * Throws {@link DatePartsError} for a value that does not satisfy the contract,\n * because an invalid value has no precision to report.\n */\nexport function precisionOf(value: unknown): Precision {\n const result = validateParts(value);\n if (!result.valid) throw new DatePartsError(result.issues);\n return result.precision;\n}\n","import { Temporal } from 'temporal-polyfill';\n\nimport {\n AmbiguousLocalTimeError,\n DatePartsError,\n MissingZoneError,\n PrecisionError,\n} from './errors.js';\nimport { readFraction } from './fraction.js';\nimport { PRECISION_LADDER } from './types.js';\nimport type { DateParts, Precision, ZoneOptions } from './types.js';\nimport { validateParts } from './validate.js';\n\n/** The narrowest Temporal type that carries everything a value states, and nothing more. */\nexport type TemporalValue =\n Temporal.PlainYearMonth | Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime;\n\ninterface Checked {\n readonly parts: DateParts;\n readonly precision: Precision;\n}\n\nfunction check(value: unknown): Checked {\n const result = validateParts(value);\n if (!result.valid) throw new DatePartsError(result.issues);\n return { parts: result.parts, precision: result.precision };\n}\n\nfunction atLeast(precision: Precision, floor: Precision): boolean {\n return PRECISION_LADDER.indexOf(precision) >= PRECISION_LADDER.indexOf(floor);\n}\n\nfunction pad(value: number, width: number): string {\n return String(value).padStart(width, '0');\n}\n\n/**\n * Signed minutes rendered as `+HH:MM` or `-HH:MM`.\n *\n * A present offset of zero renders `+00:00` rather than `Z`, so the ISO string\n * says the same thing the parts do: an offset was stated, and it was zero. An\n * ABSENT offset renders nothing at all, because unknown is not UTC.\n */\nfunction formatOffset(offsetMinutes: number): string {\n const sign = offsetMinutes < 0 ? '-' : '+';\n const absolute = Math.abs(offsetMinutes);\n return `${sign}${pad(Math.trunc(absolute / 60), 2)}:${pad(absolute % 60, 2)}`;\n}\n\n/**\n * The whole nanoseconds a fraction denotes.\n *\n * Every converter reads a fraction through the SAME function the validator uses,\n * so a value the validator has just certified can never reach a converter that\n * disagrees with it. A fraction the reader refuses is refused here with the\n * validator's own diagnostic rather than by leaking whatever Temporal makes of\n * a nonsense field.\n *\n * @throws {DatePartsError} if the fraction is not a whole number of nanoseconds.\n */\nfunction nanosecondsOf(fraction: number): number {\n const reading = readFraction(fraction);\n if (!reading.ok) throw new DatePartsError([reading.issue]);\n return reading.nanoseconds;\n}\n\n/** Sub-second digits, trimmed of trailing zeros but never to nothing. */\nfunction formatFraction(fraction: number): string {\n const digits = pad(nanosecondsOf(fraction), 9).replace(/0+$/, '');\n return `.${digits === '' ? '0' : digits}`;\n}\n\nfunction subSecondFields(fraction: number | undefined): {\n millisecond?: number;\n microsecond?: number;\n nanosecond?: number;\n} {\n if (fraction === undefined) return {};\n const nanoseconds = nanosecondsOf(fraction);\n return {\n millisecond: Math.floor(nanoseconds / 1e6),\n microsecond: Math.floor((nanoseconds % 1e6) / 1e3),\n nanosecond: nanoseconds % 1e3,\n };\n}\n\n/**\n * Render a value as an ISO 8601 string AT ITS OWN PRECISION.\n *\n * A year-only value renders `1988`; a month-only value renders `1988-05`. No\n * component the value did not state is ever added, so nothing here turns a year\n * of birth into the first of January.\n *\n * The result is a pure function of the parts. It does not read the host clock or\n * the host time zone, so it is identical under every `TZ`.\n *\n * @throws {DatePartsError} if the value does not satisfy the contract.\n */\nexport function toISO(value: unknown): string {\n const { parts, precision } = check(value);\n const suffix = parts.offsetMinutes === undefined ? '' : formatOffset(parts.offsetMinutes);\n\n let out = pad(parts.year ?? 0, 4);\n if (precision === 'year') return out;\n\n out += `-${pad(parts.month ?? 0, 2)}`;\n if (precision === 'month') return out;\n\n out += `-${pad(parts.day ?? 0, 2)}`;\n if (precision === 'day') return out;\n\n out += `T${pad(parts.hour ?? 0, 2)}`;\n if (precision === 'hour') return out + suffix;\n\n out += `:${pad(parts.minute ?? 0, 2)}`;\n if (precision === 'minute') return out + suffix;\n\n out += `:${pad(parts.second ?? 0, 2)}`;\n if (precision === 'second') return out + suffix;\n\n out += formatFraction(parts.fraction ?? 0);\n return out + suffix;\n}\n\n/**\n * Convert to the narrowest Temporal type that carries everything the value\n * states, and nothing it does not.\n *\n * | precision of the value | result |\n * |---|---|\n * | `month` | `Temporal.PlainYearMonth` |\n * | `day` | `Temporal.PlainDate`, with no time of day and no zone |\n * | `hour` and finer, no offset | `Temporal.PlainDateTime`, with no zone |\n * | `hour` and finer, with `offsetMinutes` | `Temporal.ZonedDateTime` at that exact offset |\n *\n * A year-only value has no Temporal counterpart and raises\n * {@link PrecisionError} rather than acquiring a month. Use {@link toISO} for\n * those, and {@link precisionOf} to branch.\n *\n * Components below the value's own precision are zero in the Temporal object,\n * because Temporal has no partial type below `PlainYearMonth`. The value's real\n * precision is still available from {@link precisionOf}; it is not lost, it just\n * cannot be carried by the returned object.\n *\n * @throws {DatePartsError} if the value does not satisfy the contract.\n * @throws {PrecisionError} for a year-only value.\n */\nexport function toTemporal(value: unknown): TemporalValue {\n const { parts, precision } = check(value);\n\n if (precision === 'year') {\n throw new PrecisionError(\n 'Temporal has no year-only type, and this package will not invent a month to reach one. Use toISO() to render a year-precision value, or precisionOf() to branch before converting',\n precision,\n );\n }\n\n const year = parts.year ?? 0;\n const month = parts.month ?? 0;\n\n if (precision === 'month') {\n return Temporal.PlainYearMonth.from({ year, month }, { overflow: 'reject' });\n }\n\n const day = parts.day ?? 0;\n if (precision === 'day') {\n return Temporal.PlainDate.from({ year, month, day }, { overflow: 'reject' });\n }\n\n const fields = {\n year,\n month,\n day,\n hour: parts.hour ?? 0,\n minute: parts.minute ?? 0,\n second: parts.second ?? 0,\n ...subSecondFields(parts.fraction),\n };\n\n if (parts.offsetMinutes !== undefined) {\n return Temporal.ZonedDateTime.from(\n { ...fields, timeZone: formatOffset(parts.offsetMinutes) },\n { overflow: 'reject' },\n );\n }\n\n return Temporal.PlainDateTime.from(fields, { overflow: 'reject' });\n}\n\nfunction assertKnownTimeZone(timeZone: string): void {\n try {\n // A fixed epoch, so this probe never reads the host clock or the host zone.\n Temporal.Instant.fromEpochMilliseconds(0).toZonedDateTimeISO(timeZone);\n } catch {\n throw new RangeError(\n `unknown time zone ${JSON.stringify(timeZone)}: supply an IANA identifier such as \"America/New_York\", or an offset such as \"+05:30\"`,\n );\n }\n}\n\nfunction assertUsableOffset(offsetMinutes: number): void {\n if (!Number.isInteger(offsetMinutes) || offsetMinutes < -1439 || offsetMinutes > 1439) {\n throw new RangeError(\n `offsetMinutes must be an integer in -1439-1439 (signed minutes from UTC); received ${String(offsetMinutes)}`,\n );\n }\n}\n\n/**\n * Decide which zone places this value on the timeline, or refuse.\n *\n * The value's OWN `offsetMinutes` always wins. That is what the sender said, and\n * re-zoning it on a caller's say-so would move the instant. `options` therefore\n * apply only to a value whose offset is unknown, which is exactly the case they\n * exist for.\n */\nfunction resolveTimeZone(parts: DateParts, options: ZoneOptions | undefined): string {\n if (parts.offsetMinutes !== undefined) return formatOffset(parts.offsetMinutes);\n\n if (options?.timeZone !== undefined && options.offsetMinutes !== undefined) {\n throw new TypeError(\n 'supply either timeZone or offsetMinutes, not both: two answers to one question is a guess waiting to happen',\n );\n }\n\n if (options?.offsetMinutes !== undefined) {\n assertUsableOffset(options.offsetMinutes);\n return formatOffset(options.offsetMinutes);\n }\n\n if (options?.timeZone !== undefined) {\n assertKnownTimeZone(options.timeZone);\n return options.timeZone;\n }\n\n throw new MissingZoneError(\n 'this value carries no offsetMinutes, and no zone was supplied: an absolute instant needs a zone, and this package supplies none. Pass { timeZone } or { offsetMinutes }. It will not fall back to the host time zone, to UTC, or to any other default, because a fallback turns an unknown offset into a plausible wrong instant',\n );\n}\n\n/**\n * Place a value on the timeline in a stated zone.\n *\n * Requires a time of day. A day-precision value is a calendar date, and turning\n * one into a zoned instant means choosing midnight, which is a component the\n * value does not carry. Use {@link toTemporal} for those.\n *\n * Requires a zone. If the value carries `offsetMinutes` that offset is used and\n * PRESERVED EXACTLY, never normalised to UTC or to the host zone, and `options`\n * are not consulted. Otherwise `options.timeZone` or `options.offsetMinutes`\n * must say which zone the local time is in.\n *\n * A local time that a named zone skips or repeats across a daylight-saving\n * transition raises {@link AmbiguousLocalTimeError} rather than resolving to\n * one of the two candidates, because a silent pick shifts the value by an hour.\n *\n * @throws {DatePartsError} if the value does not satisfy the contract.\n * @throws {PrecisionError} if the value states no time of day.\n * @throws {MissingZoneError} if nothing says which zone applies.\n * @throws {AmbiguousLocalTimeError} at a daylight-saving transition.\n */\nexport function toZonedDateTime(value: unknown, options?: ZoneOptions): Temporal.ZonedDateTime {\n const { parts, precision } = check(value);\n\n if (!atLeast(precision, 'hour')) {\n // A value below hour precision that also states no zone is missing BOTH, and\n // hearing about them one at a time is how a caller fixes one and comes back.\n // The precision is what is reported, because supplying a zone would not make\n // this value convertible, but the message says the zone is absent too, and\n // says as firmly as the zone refusal does that nothing will be defaulted.\n const alsoNoZone =\n parts.offsetMinutes === undefined &&\n options?.timeZone === undefined &&\n options?.offsetMinutes === undefined;\n\n throw new PrecisionError(\n `an absolute instant needs a time of day, and this value has ${precision} precision. Supplying one would mean inventing components the value does not carry. Use toTemporal() for the calendar value${\n alsoNoZone\n ? '. The zone is missing too: this value carries no offsetMinutes and no { timeZone } or { offsetMinutes } was supplied, and neither the host time zone nor UTC will be used in their place'\n : ''\n }`,\n precision,\n );\n }\n\n const timeZone = resolveTimeZone(parts, options);\n const local = Temporal.PlainDateTime.from(\n {\n year: parts.year ?? 0,\n month: parts.month ?? 0,\n day: parts.day ?? 0,\n hour: parts.hour ?? 0,\n minute: parts.minute ?? 0,\n second: parts.second ?? 0,\n ...subSecondFields(parts.fraction),\n },\n { overflow: 'reject' },\n );\n\n // Ambiguity is established by ASKING, not by catching. The two disambiguation\n // rules agree on every local time a zone maps exactly once, and disagree on\n // exactly the times a transition skips or repeats. Reading the answer off a\n // thrown message would instead relabel any other failure as a transition, and\n // a fixed offset zone, which has no transitions at all, would be told it did.\n const earliest = local.toZonedDateTime(timeZone, { disambiguation: 'earlier' });\n const latest = local.toZonedDateTime(timeZone, { disambiguation: 'later' });\n\n if (!earliest.equals(latest)) {\n // A repeated local time still reads back as itself, at two offsets. A skipped\n // one cannot: both rules move it off the gap, so the wall clock shifts.\n const skipped = !earliest.toPlainDateTime().equals(local);\n throw new AmbiguousLocalTimeError(\n skipped\n ? `${local.toString()} does not exist in ${timeZone}: a daylight-saving transition skips it, so there is no instant to return. Say which side of the transition you mean by passing that offset as { offsetMinutes }`\n : `${local.toString()} happens twice in ${timeZone}: a daylight-saving transition repeats it, at ${earliest.offset} and again at ${latest.offset}, so there is no single instant to return. Choose the one you mean and pass it as { offsetMinutes }`,\n );\n }\n\n return earliest;\n}\n\n/**\n * The absolute instant a value denotes, in a stated zone.\n *\n * Same requirements as {@link toZonedDateTime}: a time of day, and a zone that\n * something states. Nothing is defaulted.\n */\nexport function toInstant(value: unknown, options?: ZoneOptions): Temporal.Instant {\n return toZonedDateTime(value, options).toInstant();\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@cosyte/dates",
3
+ "version": "0.1.0",
4
+ "description": "Date and time utilities for the @cosyte/* healthcare parsers: precision-preserving conversion and validation that never guesses a timezone.",
5
+ "license": "MIT",
6
+ "author": "Cosyte",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "homepage": "https://docs.cosyte.com/dates",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/cosyte/dates.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/cosyte/dates/issues"
16
+ },
17
+ "keywords": [
18
+ "date",
19
+ "datetime",
20
+ "healthcare",
21
+ "hl7",
22
+ "fhir",
23
+ "temporal",
24
+ "iso-8601",
25
+ "precision",
26
+ "timezone",
27
+ "validation",
28
+ "typescript"
29
+ ],
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "types": "./dist/index.d.ts",
38
+ "files": [
39
+ "dist",
40
+ "README.md",
41
+ "LICENSE",
42
+ "CHANGELOG.md"
43
+ ],
44
+ "engines": {
45
+ "node": ">=22.0.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "provenance": true
50
+ },
51
+ "dependencies": {
52
+ "temporal-polyfill": "1.0.4"
53
+ },
54
+ "devDependencies": {
55
+ "@arethetypeswrong/cli": "0.18.4",
56
+ "@changesets/cli": "2.31.0",
57
+ "@cosyte/script-utils": "0.1.1",
58
+ "@eslint/js": "10.0.1",
59
+ "@types/node": "22.20.1",
60
+ "eslint": "10.9.1",
61
+ "prettier": "3.9.6",
62
+ "tsup": "8.5.1",
63
+ "tsx": "4.21.0",
64
+ "typescript": "5.9.3",
65
+ "typescript-eslint": "8.68.0",
66
+ "vitest": "4.1.11"
67
+ },
68
+ "scripts": {
69
+ "build": "tsup",
70
+ "changeset": "changeset",
71
+ "typecheck": "tsc --noEmit",
72
+ "lint": "eslint . --max-warnings=0",
73
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.{ts,mjs}\" \"examples/**/*.{ts,md}\" \"docs-content/**/*.{md,json}\" \"*.{ts,json,md,yml}\"",
74
+ "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.{ts,mjs}\" \"examples/**/*.{ts,md}\" \"docs-content/**/*.{md,json}\" \"*.{ts,json,md,yml}\"",
75
+ "test": "vitest run",
76
+ "test:tz": "TZ=UTC vitest run && TZ=Pacific/Kiritimati vitest run",
77
+ "examples": "tsx scripts/run-examples.ts",
78
+ "clean": "rm -rf dist coverage .tmp dist-artifacts",
79
+ "attw": "node scripts/attw.mjs --profile esm-only",
80
+ "pack:docs": "bash scripts/build-docs-artifacts.sh",
81
+ "check:no-emdash": "bash scripts/check-no-emdash.sh",
82
+ "check:no-internal-refs": "node scripts/check-no-internal-refs.mjs",
83
+ "version": "changeset version && prettier --write package.json CHANGELOG.md",
84
+ "release": "changeset publish"
85
+ }
86
+ }