daymath 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -32
- package/index.d.ts +21 -0
- package/index.js +463 -167
- package/package.json +7 -1
package/index.js
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
1
|
/** daymath — calendar date math (ISO 8601 day). date-fns-shaped. No Date. No time zones. */
|
|
2
|
-
// temporal-polyfill
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
2
|
+
// daymath reaches Temporal through `temporal-polyfill/fns`, the tree-shakable functional API, not
|
|
3
|
+
// through the `Temporal` class. A class is one unit to a bundler, because it cannot prove a method
|
|
4
|
+
// unreachable, so the class API shipped the whole polyfill for the ~20 operations used here.
|
|
5
|
+
// Measured on a three-call program by `npm run size`, shape A: 24,735 B gzip to 16,295 B, −34%.
|
|
6
|
+
// Nothing a caller can observe changes.
|
|
7
|
+
//
|
|
8
|
+
// **`getAny` is required, and `getISO` would be a defect.** `fromString` takes the calendar
|
|
9
|
+
// resolver as a REQUIRED second argument, and it is the set of calendars the bundle admits. With
|
|
10
|
+
// `getISO` the same program answers two ways: the native funcApi keeps `[u-ca=buddhist]` and reads
|
|
11
|
+
// 2569, the shim funcApi drops the annotation and reads 2026. Node 20 and bun are the shim lanes in
|
|
12
|
+
// CI. `getAny` carries the calendar data itself and answers identically on every path, measured
|
|
13
|
+
// with no global, with a BASE polyfill global, with a FULL one, and on native. That costs 4.2 kB
|
|
14
|
+
// and it is what keeps the calendar rule below a MEASUREMENT rather than a build-time list.
|
|
15
|
+
//
|
|
16
|
+
// It also retires the selection block this file used to carry. daymath no longer reads
|
|
17
|
+
// `globalThis.Temporal` at all, so the 0.5.0 defect class — a base polyfill global silently costing
|
|
18
|
+
// three of four calendars, decided by import order — cannot recur here. `fns` picks its own funcApi
|
|
19
|
+
// and `getAny` makes that choice unobservable.
|
|
20
|
+
//
|
|
21
|
+
// `Duration` is deliberately not imported. `add(record, durationRecord)` needs one, but every
|
|
22
|
+
// daymath call moves a single unit, so `addDays` / `addMonths` / `addYears` serve instead and that
|
|
23
|
+
// whole subpath stays out of the bundle.
|
|
24
|
+
import * as PlainDateFns from 'temporal-polyfill/fns/PlainDate'
|
|
25
|
+
import { getAny } from 'temporal-polyfill/fns/Calendar'
|
|
26
|
+
import * as InstantFns from 'temporal-polyfill/fns/Instant'
|
|
27
|
+
import * as ZonedFns from 'temporal-polyfill/fns/ZonedDateTime'
|
|
28
|
+
import * as NowFns from 'temporal-polyfill/fns/Now'
|
|
29
|
+
|
|
30
|
+
/** The ISO calendar record, resolved once. `isoOf` runs on every two-date export. */
|
|
31
|
+
const ISO_CALENDAR = getAny('iso8601')
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every calendar this runtime names. Read once, lowercased, because BCP-47 keys are
|
|
35
|
+
* case-insensitive while a `Map` key is not.
|
|
36
|
+
*
|
|
37
|
+
* This set is also what bounds the verdict cache below. It is closed and small — 18 ids on Node 26
|
|
38
|
+
* — so an id the runtime does not name is refused without ever being stored.
|
|
39
|
+
*
|
|
40
|
+
* `Intl.supportedValuesOf` is ES2022 and present on every runtime in `engines`.
|
|
41
|
+
*/
|
|
42
|
+
const RUNTIME_CALENDARS = new Set(
|
|
43
|
+
Intl.supportedValuesOf('calendar').map((id) => id.toLowerCase()),
|
|
44
|
+
)
|
|
6
45
|
|
|
7
46
|
/**
|
|
8
47
|
* ISO 8601 calendar day string:
|
|
@@ -82,7 +121,14 @@ function hasZoneAnnotation(text) {
|
|
|
82
121
|
const ZONE_LIKE =
|
|
83
122
|
/^(?:(?![Tt]\d)[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)*|[+-]\d{2}(?::?\d{2})?)$/u
|
|
84
123
|
|
|
85
|
-
|
|
124
|
+
// Two different things wear the name PlainDate here, and keeping them apart is the whole type
|
|
125
|
+
// story of this file. `PlainDateRecord` is daymath's INTERNAL value, and the name is the polyfill's
|
|
126
|
+
// own for it. `PlainDate` is the public INPUT type, a real `Temporal.PlainDate` object from any
|
|
127
|
+
// implementation, which callers still pass and `isPlainDate` still recognises by brand. daymath
|
|
128
|
+
// never returns one.
|
|
129
|
+
/** @typedef {import('temporal-polyfill/fns/PlainDate').Record} PlainDateRecord */
|
|
130
|
+
/** @typedef {import('temporal-polyfill').Temporal.PlainDate} PlainDate */
|
|
131
|
+
/** @typedef {string | PlainDate} DayInput */
|
|
86
132
|
/**
|
|
87
133
|
* @typedef {object} Interval
|
|
88
134
|
* @property {DayInput} start
|
|
@@ -100,11 +146,9 @@ const ZONE_LIKE =
|
|
|
100
146
|
/**
|
|
101
147
|
* The bare ISO day inside a string, or `null` if there is not one.
|
|
102
148
|
*
|
|
103
|
-
* An annotation is dropped,
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* it names the very calendar daymath reads, so refusing a caller's own
|
|
107
|
-
* round-trip would be arbitrary.
|
|
149
|
+
* An annotation is dropped here, never judged. `supportedCalendar` owns that rule, and the two do
|
|
150
|
+
* not run in a fixed order: `toPlainDate` judges first, and `day()` calls this first. That is safe
|
|
151
|
+
* precisely because this function strips the annotation without reading it.
|
|
108
152
|
*
|
|
109
153
|
* One predicate, because `toPlainDate` and `day()` both ask this question. They
|
|
110
154
|
* asked it separately once, and day() alone then refused a string that every
|
|
@@ -119,21 +163,139 @@ function bareDay(text) {
|
|
|
119
163
|
}
|
|
120
164
|
|
|
121
165
|
/**
|
|
122
|
-
*
|
|
166
|
+
* Nine probe dates for the calendar rule below.
|
|
167
|
+
*
|
|
168
|
+
* They span 1900 to 2100, they sit in different months, and two pairs straddle a Japanese era
|
|
169
|
+
* boundary: 1989-01-07/08 is Showa 64 into Heisei 1, and 2019-04-30/05-01 is Heisei 31 into
|
|
170
|
+
* Reiwa 1. An era change *inside* one ISO year is the case a single probe cannot see, and it is
|
|
171
|
+
* exactly what separates a year label from a year renumbering.
|
|
172
|
+
*/
|
|
173
|
+
const CALENDAR_PROBES = [
|
|
174
|
+
'1900-01-01',
|
|
175
|
+
'1900-07-01',
|
|
176
|
+
'1989-01-07',
|
|
177
|
+
'1989-01-08',
|
|
178
|
+
'2019-04-30',
|
|
179
|
+
'2019-05-01',
|
|
180
|
+
'2026-01-31',
|
|
181
|
+
'2026-12-31',
|
|
182
|
+
'2100-06-15',
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
/** @type {Map<string, {offset: number} | {reason: 'unknown' | 'renumbers'}>} */
|
|
186
|
+
const calendarRuleCache = new Map()
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Is this a calendar the runtime actually names?
|
|
190
|
+
*
|
|
191
|
+
* **This is what bounds `calendarRuleCache`, and it is a memory-exhaustion fix.** The cache keys on
|
|
192
|
+
* whatever sits between `[u-ca=` and `]`, which is caller input.
|
|
193
|
+
*
|
|
194
|
+
* A shape guard was tried first and was only half a fix. It bounded the key LENGTH and not the
|
|
195
|
+
* entry COUNT, so any BCP-47-shaped string still bought a permanent entry: 800,000 distinct
|
|
196
|
+
* guard-passing ids retained 76.8 MB, growing linearly and never released. Case made it worse,
|
|
197
|
+
* because the shape test ignored case while the `Map` key did not, so one name had 256 keys.
|
|
198
|
+
*
|
|
199
|
+
* A closed set fixes both at once. `RUNTIME_CALENDARS` holds 18 ids on Node 26, so entries can
|
|
200
|
+
* never exceed what the runtime has, and a lowercased key collapses the case variants. It also
|
|
201
|
+
* needs no length cap and no pattern, which removes the last quadratic-regex risk from this file.
|
|
202
|
+
*
|
|
203
|
+
* The quiet path was `isValid`, which swallows the `RangeError`, so a loop raised nothing and
|
|
204
|
+
* logged nothing.
|
|
205
|
+
* @param {string} lowerId
|
|
206
|
+
*/
|
|
207
|
+
function runtimeKnowsCalendar(lowerId) {
|
|
208
|
+
return RUNTIME_CALENDARS.has(lowerId)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Measure whether a calendar only relabels the year.
|
|
123
213
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
214
|
+
* **This is a method, not a list.** Nothing here names a calendar, so a calendar CLDR adds later
|
|
215
|
+
* is admitted or refused by the same measurement, with no code change. Today it admits
|
|
216
|
+
* `buddhist` (+543), `roc` (−1911), `japanese` (0) and `gregory` (0), and refuses the other
|
|
217
|
+
* eleven the runtime knows.
|
|
218
|
+
*
|
|
219
|
+
* Two conditions, both required on every probe:
|
|
220
|
+
*
|
|
221
|
+
* 1. **Month and day equal the ISO fields.** A month-count test would be wrong: `hebrew` is
|
|
222
|
+
* lunisolar, so `monthsInYear` is 13 in 2024, 12 in 2025 and 13 in 2027. Comparing the fields
|
|
223
|
+
* is correct in every year.
|
|
224
|
+
* 2. **The year offset is constant.** `japanese` and `roc` pass condition 1, and Temporal reports
|
|
225
|
+
* a continuous year for both, so both are pure labels — `roc` 1900 is −11, not "12 before".
|
|
226
|
+
* Reading the year from `Intl` instead would fail here, because `Intl` reports the *era* year:
|
|
227
|
+
* Reiwa 8 and B.R.O.C. 12. Temporal is the reference, so Temporal is what this asks.
|
|
228
|
+
*
|
|
229
|
+
* The verdict is cached per calendar. It cannot change while the process runs.
|
|
230
|
+
* @param {string} calendar
|
|
231
|
+
*/
|
|
232
|
+
function calendarRule(calendar) {
|
|
233
|
+
const cached = calendarRuleCache.get(calendar)
|
|
234
|
+
if (cached !== undefined) return cached
|
|
235
|
+
/** @type {{offset: number} | {reason: 'unknown' | 'renumbers'}} */
|
|
236
|
+
let verdict = { reason: 'renumbers' }
|
|
237
|
+
try {
|
|
238
|
+
const offsets = new Set()
|
|
239
|
+
// `getAny(calendar)` throws for an id the polyfill cannot build, which is the `unknown` case
|
|
240
|
+
// below. It is resolved once rather than per probe.
|
|
241
|
+
const calendarRecord = getAny(calendar)
|
|
242
|
+
for (const probe of CALENDAR_PROBES) {
|
|
243
|
+
const iso = PlainDateFns.fromString(probe, getAny)
|
|
244
|
+
const dated = PlainDateFns.withCalendar(iso, calendarRecord)
|
|
245
|
+
if (dated.month !== iso.month || dated.day !== iso.day) {
|
|
246
|
+
offsets.clear()
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
offsets.add(dated.year - iso.year)
|
|
250
|
+
}
|
|
251
|
+
// The offset is the discriminant — `'offset' in verdict` is what accepts — and the number
|
|
252
|
+
// itself is kept unread on purpose, so a debugger shows WHY a calendar passed.
|
|
253
|
+
if (offsets.size === 1) verdict = { offset: [...offsets][0] }
|
|
254
|
+
} catch {
|
|
255
|
+
// The runtime cannot build this calendar at all, which is a different message for the caller.
|
|
256
|
+
verdict = { reason: 'unknown' }
|
|
257
|
+
}
|
|
258
|
+
calendarRuleCache.set(calendar, verdict)
|
|
259
|
+
return verdict
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Accept a calendar that only relabels the year, and refuse one that renumbers.
|
|
264
|
+
*
|
|
265
|
+
* Checked on the *string*, before any parse, so the outcome cannot depend on where the annotation
|
|
266
|
+
* sits. Returns the calendar for `toPlainDate` to carry, or `undefined` for plain ISO.
|
|
267
|
+
*
|
|
268
|
+
* `[u-ca=iso8601]` returns `undefined` rather than carrying: Temporal writes it itself for
|
|
269
|
+
* `toString({ calendarName: 'always' })`, and daymath already answers in it, so there is nothing
|
|
270
|
+
* to relabel.
|
|
127
271
|
* @param {string} text
|
|
128
272
|
* @param {string} label
|
|
273
|
+
* @returns {string | undefined}
|
|
129
274
|
*/
|
|
130
|
-
function
|
|
275
|
+
function supportedCalendar(text, label) {
|
|
131
276
|
const annotated = calendarAnnotation(text)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
277
|
+
// A bare `return` rather than `return undefined`: oxlint's no-useless-undefined forbids the
|
|
278
|
+
// explicit spelling, and "nothing to carry" is what both of these mean.
|
|
279
|
+
if (annotated === null) return
|
|
280
|
+
const { calendar } = annotated
|
|
281
|
+
// Lowercased once. BCP-47 keys are case-insensitive, and this is also the cache key, so the case
|
|
282
|
+
// variants of one name must collapse to one entry rather than 256.
|
|
283
|
+
const lowerId = calendar.toLowerCase()
|
|
284
|
+
if (lowerId === 'iso8601') return
|
|
285
|
+
// Membership FIRST, so an id the runtime does not name is never stored. See runtimeKnowsCalendar.
|
|
286
|
+
// One throw, not two: an unknown id and a renumbering one differ only in the reason.
|
|
287
|
+
const verdict = runtimeKnowsCalendar(lowerId)
|
|
288
|
+
? calendarRule(lowerId)
|
|
289
|
+
: /** @type {const} */ ({ reason: 'unknown' })
|
|
290
|
+
if ('offset' in verdict) return calendar
|
|
291
|
+
const why =
|
|
292
|
+
verdict.reason === 'unknown'
|
|
293
|
+
? 'is not a calendar this runtime knows'
|
|
294
|
+
: 'renumbers months or days, so daymath cannot answer a day in it'
|
|
295
|
+
// The id is sliced because it is caller input, so a megabyte in cannot become a megabyte of error.
|
|
296
|
+
throw new RangeError(
|
|
297
|
+
`daymath: ${label} calendar ${JSON.stringify(calendar.slice(0, 40))} ${why} (convert with withCalendar('iso8601'))`,
|
|
298
|
+
)
|
|
137
299
|
}
|
|
138
300
|
|
|
139
301
|
/**
|
|
@@ -142,7 +304,7 @@ function assertIsoCalendar(text, label) {
|
|
|
142
304
|
* `instanceof` recognises only one of those. Every Temporal puts this tag on
|
|
143
305
|
* `PlainDate.prototype` as a non-writable property, so it is the portable brand.
|
|
144
306
|
* @param {unknown} value
|
|
145
|
-
* @returns {value is
|
|
307
|
+
* @returns {value is PlainDate} a predicate, so callers narrow
|
|
146
308
|
*/
|
|
147
309
|
function isPlainDate(value) {
|
|
148
310
|
return Object.prototype.toString.call(value) === '[object Temporal.PlainDate]'
|
|
@@ -154,21 +316,12 @@ function isPlainDate(value) {
|
|
|
154
316
|
* A PlainDate becomes its ISO day string first, so every input reaches one
|
|
155
317
|
* validation path and daymath owns the resulting instance.
|
|
156
318
|
*
|
|
157
|
-
* A
|
|
158
|
-
* the
|
|
159
|
-
*
|
|
160
|
-
* `2026-01-31[u-ca=buddhist]` is year 2569, and `getYear` would answer `2026`
|
|
161
|
-
* where the caller's own object says `2569`. A string carrying the annotation is
|
|
162
|
-
* refused for the same reason, and the error names both the calendar it found
|
|
163
|
-
* and the way through, `withCalendar('iso8601')`.
|
|
164
|
-
*
|
|
165
|
-
* `[u-ca=iso8601]` is the exception: it is accepted and dropped. Temporal writes
|
|
166
|
-
* it itself for `toString({ calendarName: 'always' })`, and it names the very
|
|
167
|
-
* calendar daymath reads, so refusing a caller's own round-trip would be
|
|
168
|
-
* arbitrary.
|
|
319
|
+
* A calendar annotation is judged by `supportedCalendar` and then CARRIED, so `getYear` answers
|
|
320
|
+
* the caller's own number: `2026-01-31[u-ca=buddhist]` reads 2569, not 2026. The rule and the
|
|
321
|
+
* reasons live on `supportedCalendar`; this function only applies the verdict.
|
|
169
322
|
* @param {unknown} value
|
|
170
323
|
* @param {string} label
|
|
171
|
-
* @returns {
|
|
324
|
+
* @returns {PlainDateRecord}
|
|
172
325
|
*/
|
|
173
326
|
function toPlainDate(value, label = 'date') {
|
|
174
327
|
if (value instanceof Date) {
|
|
@@ -184,7 +337,7 @@ function toPlainDate(value, label = 'date') {
|
|
|
184
337
|
)
|
|
185
338
|
}
|
|
186
339
|
// Before the shape check: an annotated day is well formed, not malformed.
|
|
187
|
-
|
|
340
|
+
const calendar = supportedCalendar(text, label)
|
|
188
341
|
const bare = bareDay(text)
|
|
189
342
|
if (bare === null) {
|
|
190
343
|
throw new RangeError(
|
|
@@ -192,7 +345,15 @@ function toPlainDate(value, label = 'date') {
|
|
|
192
345
|
)
|
|
193
346
|
}
|
|
194
347
|
try {
|
|
195
|
-
|
|
348
|
+
// The bare day is parsed with the ISO resolver's own entry point. `getAny` is passed as the
|
|
349
|
+
// resolver rather than called, because `fromString` calls it with whatever the string names,
|
|
350
|
+
// and a bare day names nothing.
|
|
351
|
+
const plain = PlainDateFns.fromString(bare, getAny)
|
|
352
|
+
// The annotation rides along, so getYear answers 2569 for a Buddhist day and every
|
|
353
|
+
// returned string keeps the calendar the caller named.
|
|
354
|
+
return calendar === undefined
|
|
355
|
+
? plain
|
|
356
|
+
: PlainDateFns.withCalendar(plain, getAny(calendar))
|
|
196
357
|
} catch (err) {
|
|
197
358
|
throw new RangeError(`daymath: invalid ${label} ${JSON.stringify(text)}`, {
|
|
198
359
|
cause: err,
|
|
@@ -200,9 +361,25 @@ function toPlainDate(value, label = 'date') {
|
|
|
200
361
|
}
|
|
201
362
|
}
|
|
202
363
|
|
|
203
|
-
/**
|
|
364
|
+
/**
|
|
365
|
+
* The same day in ISO, for measurement only.
|
|
366
|
+
*
|
|
367
|
+
* Temporal refuses `since` and `until` across two calendars with `Mismatched calendars`, and its
|
|
368
|
+
* `equals` compares the calendar as well as the day. Neither matters to a day count: a day is the
|
|
369
|
+
* same day whatever the year is labelled, so daymath normalises and answers. Only the exports that
|
|
370
|
+
* *read* or *write* a field honour the label.
|
|
371
|
+
* @param {PlainDateRecord} plain
|
|
372
|
+
* @returns {PlainDateRecord}
|
|
373
|
+
*/
|
|
374
|
+
function isoOf(plain) {
|
|
375
|
+
return plain.calendarId === 'iso8601'
|
|
376
|
+
? plain
|
|
377
|
+
: PlainDateFns.withCalendar(plain, ISO_CALENDAR)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** @param {PlainDateRecord} plain @returns {string} */
|
|
204
381
|
function toDayString(plain) {
|
|
205
|
-
return
|
|
382
|
+
return PlainDateFns.toString(plain)
|
|
206
383
|
}
|
|
207
384
|
|
|
208
385
|
/** @param {unknown} n @param {string} label */
|
|
@@ -243,14 +420,29 @@ function guardRange(label, op) {
|
|
|
243
420
|
/**
|
|
244
421
|
* Shared body for every add/sub function. Each caller passes its own name, so
|
|
245
422
|
* the message never reports a function the caller did not call.
|
|
423
|
+
*
|
|
424
|
+
* The unit is a parameter rather than a duration object on purpose. `PlainDateFns.add` would need a
|
|
425
|
+
* `DurationRecord`, which means importing `fns/Duration`, and every daymath caller moves exactly
|
|
426
|
+
* one unit. `addDays` / `addMonths` / `addYears` take a plain number and keep that subpath out of
|
|
427
|
+
* the bundle. Subtraction is a negative amount, exactly as it was when this passed `-amount`.
|
|
428
|
+
*
|
|
429
|
+
* `addMonths` and `addYears` clamp by default, which is daymath's one overflow rule and the same
|
|
430
|
+
* behaviour the class API gave: 31 January plus one month is 28 February.
|
|
246
431
|
* @param {DayInput} date
|
|
247
|
-
* @param {
|
|
432
|
+
* @param {'days'|'months'|'years'} unit
|
|
433
|
+
* @param {number} amount
|
|
248
434
|
* @param {string} label
|
|
249
435
|
* @returns {string}
|
|
250
436
|
*/
|
|
251
|
-
function addDuration(date,
|
|
437
|
+
function addDuration(date, unit, amount, label) {
|
|
252
438
|
const d = toPlainDate(date)
|
|
253
|
-
|
|
439
|
+
const move =
|
|
440
|
+
unit === 'days'
|
|
441
|
+
? PlainDateFns.addDays
|
|
442
|
+
: unit === 'months'
|
|
443
|
+
? PlainDateFns.addMonths
|
|
444
|
+
: PlainDateFns.addYears
|
|
445
|
+
return guardRange(label, () => toDayString(move(d, amount)))
|
|
254
446
|
}
|
|
255
447
|
|
|
256
448
|
/** @param {unknown} dates */
|
|
@@ -274,7 +466,7 @@ function weekStartsOnFrom(options) {
|
|
|
274
466
|
|
|
275
467
|
/**
|
|
276
468
|
* @param {unknown} interval
|
|
277
|
-
* @returns {{ start:
|
|
469
|
+
* @returns {{ start: PlainDateRecord, end: PlainDateRecord }}
|
|
278
470
|
*/
|
|
279
471
|
function toInterval(interval) {
|
|
280
472
|
// `typeof x === 'object'` alone let a Date, an array or a PlainDate through,
|
|
@@ -315,6 +507,16 @@ function toInterval(interval) {
|
|
|
315
507
|
* - a string carrying a `[Zone]` annotation, which names its own zone, so it
|
|
316
508
|
* answers its own civil day and the `tz` default never applies
|
|
317
509
|
*
|
|
510
|
+
* **day() is the normaliser, and that is the one rule to hold: a moment
|
|
511
|
+
* converts to a plain ISO day, and so does a day.** A `[u-ca=…]` annotation is
|
|
512
|
+
* accepted wherever the other 68 exports accept it, and then dropped from the
|
|
513
|
+
* result. Those exports carry it, because the caller asked for that numbering;
|
|
514
|
+
* day() exists to hand back the canonical form. It cannot do both, because an
|
|
515
|
+
* `Instant` has no fields for a calendar to renumber, so an annotation on
|
|
516
|
+
* `'…T20:00:00Z[u-ca=buddhist]'` is inert and that path has nothing to carry.
|
|
517
|
+
* Carrying only on the `[Zone]` path would make the same annotation behave two
|
|
518
|
+
* ways in one function.
|
|
519
|
+
*
|
|
318
520
|
* `'11/12/2026'` is refused. Nobody can tell November from December in it.
|
|
319
521
|
* `'2026-08-08T12:00'` is refused too: no offset and no zone, so daymath would
|
|
320
522
|
* have to pick one, and it will not pick on the caller's behalf. Name the zone
|
|
@@ -335,7 +537,10 @@ function toInterval(interval) {
|
|
|
335
537
|
*
|
|
336
538
|
* @param {Date | number | DayInput | null} [moment] instant, epoch ms, day, or a zone
|
|
337
539
|
* @param {string} [tz] IANA time zone id, e.g. `'utc'`, `'Asia/Tokyo'`
|
|
338
|
-
* @returns {string} `YYYY-MM-DD`
|
|
540
|
+
* @returns {string} a plain ISO day. `YYYY-MM-DD`, or expanded `±YYYYYY-MM-DD` outside years
|
|
541
|
+
* 0000-9999 — the annotation said `YYYY-MM-DD` alone, which was already wrong for
|
|
542
|
+
* `day('+010000-01-01')`. **Never carries `[u-ca=…]`.** day() is the normaliser, so a calendar
|
|
543
|
+
* annotation is accepted on input and dropped from the result. Every other export carries it.
|
|
339
544
|
* @throws {TypeError} If `moment` is not one of the accepted shapes
|
|
340
545
|
* @throws {RangeError} On an Invalid Date, a non-finite number, or an unknown zone
|
|
341
546
|
* @example day() // '2026-08-08' now, UTC
|
|
@@ -355,9 +560,9 @@ export function day(moment, tz) {
|
|
|
355
560
|
const isMoment = isDay || moment instanceof Date || typeof moment === 'number'
|
|
356
561
|
|
|
357
562
|
let zone = tz
|
|
358
|
-
/** @type {
|
|
563
|
+
/** @type {import('temporal-polyfill/fns/Instant').Record | undefined} */
|
|
359
564
|
let instant
|
|
360
|
-
/** @type {
|
|
565
|
+
/** @type {import('temporal-polyfill/fns/ZonedDateTime').Record | undefined} */
|
|
361
566
|
let zoned
|
|
362
567
|
if (!isMoment && moment !== undefined && moment !== null) {
|
|
363
568
|
if (typeof moment !== 'string') {
|
|
@@ -376,20 +581,20 @@ export function day(moment, tz) {
|
|
|
376
581
|
// `'…[Asia/Tokoy]'` is a typo. Falling back to the instant would answer a
|
|
377
582
|
// UTC day for both, silently, which is the defect this branch exists to fix.
|
|
378
583
|
if (hasZoneAnnotation(moment)) {
|
|
379
|
-
// A calendar is
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
// message depend on the runtime.
|
|
385
|
-
assertIsoCalendar(moment, 'date')
|
|
584
|
+
// A calendar is judged only where it is applied. With a zone bracket it is: the fields get
|
|
585
|
+
// renumbered, and `toPlainDate()` carries the annotation into daymath's own output. Judged
|
|
586
|
+
// on the string, before the parse, so the message cannot depend on the runtime. A calendar
|
|
587
|
+
// that only relabels the year passes here, exactly as it does through toPlainDate.
|
|
588
|
+
supportedCalendar(moment, 'date')
|
|
386
589
|
if (tz !== undefined && tz !== null) {
|
|
387
590
|
throw new TypeError(
|
|
388
591
|
`daymath: day() got two time zones, ${JSON.stringify(moment)} and ${JSON.stringify(tz)}`,
|
|
389
592
|
)
|
|
390
593
|
}
|
|
391
594
|
try {
|
|
392
|
-
zoned
|
|
595
|
+
// `getAny` again, for the same reason it is used everywhere else: a zoned string can carry
|
|
596
|
+
// a calendar annotation, and the shim funcApi would drop it under `getISO`.
|
|
597
|
+
zoned = ZonedFns.fromString(moment, getAny)
|
|
393
598
|
} catch (err) {
|
|
394
599
|
throw new RangeError(
|
|
395
600
|
`daymath: day() could not read ${JSON.stringify(moment)} in the time zone it names`,
|
|
@@ -405,7 +610,7 @@ export function day(moment, tz) {
|
|
|
405
610
|
// year, month or day for a calendar to renumber, and without a zone
|
|
406
611
|
// bracket Temporal will not build anything that does. Refusing it would
|
|
407
612
|
// reject a right answer for a reason that cannot apply.
|
|
408
|
-
instant =
|
|
613
|
+
instant = InstantFns.fromString(moment)
|
|
409
614
|
} catch {
|
|
410
615
|
// Not a moment, so the string must be a zone — decided by shape, before
|
|
411
616
|
// Temporal sees it. Temporal's zone grammar also accepts a whole
|
|
@@ -447,10 +652,10 @@ export function day(moment, tz) {
|
|
|
447
652
|
// whatever the moment is. A caller mapping rows that are sometimes a Date and
|
|
448
653
|
// sometimes a day string would otherwise see the typo only on some rows.
|
|
449
654
|
//
|
|
450
|
-
// `ZonedDateTime.
|
|
655
|
+
// `ZonedDateTime.fromFields` accepts exactly the zone ids `Now` accepts and reads
|
|
451
656
|
// no clock, so a day input stays a pure function.
|
|
452
657
|
try {
|
|
453
|
-
|
|
658
|
+
ZonedFns.fromFields({ timeZone: zone, year: 1970, month: 1, day: 1 })
|
|
454
659
|
} catch (err) {
|
|
455
660
|
throw new RangeError(
|
|
456
661
|
`daymath: day() got an unknown time zone ${JSON.stringify(zone)}`,
|
|
@@ -460,27 +665,39 @@ export function day(moment, tz) {
|
|
|
460
665
|
|
|
461
666
|
// A day carries no time, so a zone has nothing to shift. Applying one would
|
|
462
667
|
// invent a moment the caller never gave.
|
|
463
|
-
|
|
668
|
+
//
|
|
669
|
+
// `isoOf` is what makes day() the normaliser. Every other export carries a
|
|
670
|
+
// `[u-ca=…]` annotation through, because the caller asked for that numbering.
|
|
671
|
+
// day() is the door: something that is not a plain ISO day goes in, and a
|
|
672
|
+
// plain ISO day comes out. Carrying it here also could not be made
|
|
673
|
+
// consistent, because an `Instant` has no fields for a calendar to renumber,
|
|
674
|
+
// so that path has nothing to carry and would answer bare ISO regardless.
|
|
675
|
+
if (isDay) return toDayString(isoOf(toPlainDate(moment)))
|
|
464
676
|
|
|
465
677
|
// The string named its own zone, so that zone decides the day, not the
|
|
466
678
|
// default. This is the one path where `zone` is deliberately not consulted.
|
|
467
679
|
//
|
|
468
|
-
// The result still goes through `toPlainDate
|
|
469
|
-
//
|
|
470
|
-
// returning directly emitted `'2026-08-08[u-ca=buddhist]'` — a value daymath
|
|
471
|
-
// itself refuses. Every path adjudicates the calendar in one place or none.
|
|
680
|
+
// The result still goes through `toPlainDate`, so the calendar is adjudicated
|
|
681
|
+
// in one place, and then through `isoOf` for the reason above.
|
|
472
682
|
if (zoned !== undefined) {
|
|
473
|
-
|
|
474
|
-
|
|
683
|
+
// `toPlainDate` is re-entered with the STRING, not the record, because daymath's own funnel
|
|
684
|
+
// takes a day string or a `Temporal.PlainDate`, and an fns record is neither. `PlainDateFns.toString`
|
|
685
|
+
// writes the same day the class API's `toPlainDate()` produced, annotation and all.
|
|
686
|
+
const plain = guardRange('day', () =>
|
|
687
|
+
PlainDateFns.toString(ZonedFns.toPlainDate(zoned)),
|
|
688
|
+
)
|
|
689
|
+
return toDayString(isoOf(toPlainDate(plain)))
|
|
475
690
|
}
|
|
476
691
|
|
|
477
692
|
if (instant !== undefined) {
|
|
478
693
|
return guardRange('day', () =>
|
|
479
|
-
|
|
694
|
+
PlainDateFns.toString(
|
|
695
|
+
ZonedFns.toPlainDate(InstantFns.toZonedDateTimeISO(instant, zone)),
|
|
696
|
+
),
|
|
480
697
|
)
|
|
481
698
|
}
|
|
482
699
|
|
|
483
|
-
if (!isMoment) return
|
|
700
|
+
if (!isMoment) return PlainDateFns.toString(NowFns.plainDateISO(zone))
|
|
484
701
|
|
|
485
702
|
// Truncate, because `new Date(n)` truncates, and the contract here is that a
|
|
486
703
|
// number reads exactly as it does. Verified equal on positive and negative
|
|
@@ -492,10 +709,11 @@ export function day(moment, tz) {
|
|
|
492
709
|
? Math.trunc(moment)
|
|
493
710
|
: /** @type {Date} */ (moment).getTime()
|
|
494
711
|
return guardRange('day', () =>
|
|
495
|
-
|
|
496
|
-
.
|
|
497
|
-
|
|
498
|
-
|
|
712
|
+
PlainDateFns.toString(
|
|
713
|
+
ZonedFns.toPlainDate(
|
|
714
|
+
InstantFns.toZonedDateTimeISO(InstantFns.fromEpochMilliseconds(epochMs), zone),
|
|
715
|
+
),
|
|
716
|
+
),
|
|
499
717
|
)
|
|
500
718
|
}
|
|
501
719
|
|
|
@@ -555,7 +773,7 @@ export function format(date, pattern = 'yyyy-MM-dd') {
|
|
|
555
773
|
*/
|
|
556
774
|
export function addDays(date, amount) {
|
|
557
775
|
assertFiniteNumber(amount, 'amount')
|
|
558
|
-
return addDuration(date,
|
|
776
|
+
return addDuration(date, 'days', amount, 'addDays')
|
|
559
777
|
}
|
|
560
778
|
|
|
561
779
|
/**
|
|
@@ -565,7 +783,7 @@ export function addDays(date, amount) {
|
|
|
565
783
|
*/
|
|
566
784
|
export function subDays(date, amount) {
|
|
567
785
|
assertFiniteNumber(amount, 'amount')
|
|
568
|
-
return addDuration(date,
|
|
786
|
+
return addDuration(date, 'days', -amount, 'subDays')
|
|
569
787
|
}
|
|
570
788
|
|
|
571
789
|
/**
|
|
@@ -577,7 +795,7 @@ export function addWeeks(date, amount) {
|
|
|
577
795
|
assertFiniteNumber(amount, 'amount')
|
|
578
796
|
const days = amount * 7 // re-check: 7x a finite amount can still reach Infinity
|
|
579
797
|
assertFiniteNumber(days, 'amount')
|
|
580
|
-
return addDuration(date,
|
|
798
|
+
return addDuration(date, 'days', days, 'addWeeks')
|
|
581
799
|
}
|
|
582
800
|
|
|
583
801
|
/**
|
|
@@ -589,7 +807,7 @@ export function subWeeks(date, amount) {
|
|
|
589
807
|
assertFiniteNumber(amount, 'amount')
|
|
590
808
|
const days = -amount * 7
|
|
591
809
|
assertFiniteNumber(days, 'amount')
|
|
592
|
-
return addDuration(date,
|
|
810
|
+
return addDuration(date, 'days', days, 'subWeeks')
|
|
593
811
|
}
|
|
594
812
|
|
|
595
813
|
/**
|
|
@@ -600,7 +818,7 @@ export function subWeeks(date, amount) {
|
|
|
600
818
|
*/
|
|
601
819
|
export function addMonths(date, amount) {
|
|
602
820
|
assertFiniteNumber(amount, 'amount')
|
|
603
|
-
return addDuration(date,
|
|
821
|
+
return addDuration(date, 'months', amount, 'addMonths')
|
|
604
822
|
}
|
|
605
823
|
|
|
606
824
|
/**
|
|
@@ -610,7 +828,7 @@ export function addMonths(date, amount) {
|
|
|
610
828
|
*/
|
|
611
829
|
export function subMonths(date, amount) {
|
|
612
830
|
assertFiniteNumber(amount, 'amount')
|
|
613
|
-
return addDuration(date,
|
|
831
|
+
return addDuration(date, 'months', -amount, 'subMonths')
|
|
614
832
|
}
|
|
615
833
|
|
|
616
834
|
/**
|
|
@@ -620,7 +838,7 @@ export function subMonths(date, amount) {
|
|
|
620
838
|
*/
|
|
621
839
|
export function addYears(date, amount) {
|
|
622
840
|
assertFiniteNumber(amount, 'amount')
|
|
623
|
-
return addDuration(date,
|
|
841
|
+
return addDuration(date, 'years', amount, 'addYears')
|
|
624
842
|
}
|
|
625
843
|
|
|
626
844
|
/**
|
|
@@ -630,7 +848,7 @@ export function addYears(date, amount) {
|
|
|
630
848
|
*/
|
|
631
849
|
export function subYears(date, amount) {
|
|
632
850
|
assertFiniteNumber(amount, 'amount')
|
|
633
|
-
return addDuration(date,
|
|
851
|
+
return addDuration(date, 'years', -amount, 'subYears')
|
|
634
852
|
}
|
|
635
853
|
|
|
636
854
|
/**
|
|
@@ -642,7 +860,7 @@ export function addQuarters(date, amount) {
|
|
|
642
860
|
assertFiniteNumber(amount, 'amount')
|
|
643
861
|
const months = amount * 3
|
|
644
862
|
assertFiniteNumber(months, 'amount')
|
|
645
|
-
return addDuration(date,
|
|
863
|
+
return addDuration(date, 'months', months, 'addQuarters')
|
|
646
864
|
}
|
|
647
865
|
|
|
648
866
|
/**
|
|
@@ -654,7 +872,7 @@ export function subQuarters(date, amount) {
|
|
|
654
872
|
assertFiniteNumber(amount, 'amount')
|
|
655
873
|
const months = -amount * 3
|
|
656
874
|
assertFiniteNumber(months, 'amount')
|
|
657
|
-
return addDuration(date,
|
|
875
|
+
return addDuration(date, 'months', months, 'subQuarters')
|
|
658
876
|
}
|
|
659
877
|
|
|
660
878
|
// ─── getters / setters (date-fns / Date month & weekday indexing) ─
|
|
@@ -687,27 +905,51 @@ export function getDate(date) {
|
|
|
687
905
|
* @returns {number}
|
|
688
906
|
*/
|
|
689
907
|
export function getDay(date) {
|
|
690
|
-
return toPlainDate(date)
|
|
908
|
+
return PlainDateFns.dayOfWeek(toPlainDate(date))
|
|
691
909
|
}
|
|
692
910
|
|
|
693
911
|
/** @param {DayInput} date @returns {number} */
|
|
694
912
|
export function getDayOfYear(date) {
|
|
695
|
-
return toPlainDate(date)
|
|
913
|
+
return PlainDateFns.dayOfYear(toPlainDate(date))
|
|
696
914
|
}
|
|
697
915
|
|
|
698
916
|
/** @param {DayInput} date @returns {number} */
|
|
699
917
|
export function getDaysInMonth(date) {
|
|
700
|
-
return toPlainDate(date)
|
|
918
|
+
return PlainDateFns.daysInMonth(toPlainDate(date))
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Quarter of a record daymath already owns.
|
|
923
|
+
*
|
|
924
|
+
* The class API needed no such helper, because a `Temporal.PlainDate` passed back into
|
|
925
|
+
* `toPlainDate` was recognised by `isPlainDate` and survived the round trip. An fns record is not
|
|
926
|
+
* a `Temporal.PlainDate` and is correctly refused, so every internal caller now works on the
|
|
927
|
+
* record. That was always the honest shape: re-validating a value daymath just built is waste.
|
|
928
|
+
* @param {PlainDateRecord} plain
|
|
929
|
+
* @returns {number}
|
|
930
|
+
*/
|
|
931
|
+
function quarterOf(plain) {
|
|
932
|
+
return Math.ceil(plain.month / 3)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Calendar month index between two records already normalised to ISO.
|
|
937
|
+
* @param {PlainDateRecord} left
|
|
938
|
+
* @param {PlainDateRecord} right
|
|
939
|
+
* @returns {number}
|
|
940
|
+
*/
|
|
941
|
+
function calendarMonthsBetween(left, right) {
|
|
942
|
+
return (left.year - right.year) * 12 + (left.month - right.month)
|
|
701
943
|
}
|
|
702
944
|
|
|
703
945
|
/** Quarter 1…4. @param {DayInput} date @returns {number} */
|
|
704
946
|
export function getQuarter(date) {
|
|
705
|
-
return
|
|
947
|
+
return quarterOf(toPlainDate(date))
|
|
706
948
|
}
|
|
707
949
|
|
|
708
950
|
/** @param {DayInput} date @returns {boolean} */
|
|
709
951
|
export function isLeapYear(date) {
|
|
710
|
-
return toPlainDate(date)
|
|
952
|
+
return PlainDateFns.inLeapYear(toPlainDate(date))
|
|
711
953
|
}
|
|
712
954
|
|
|
713
955
|
/**
|
|
@@ -718,7 +960,7 @@ export function isLeapYear(date) {
|
|
|
718
960
|
export function setYear(date, year) {
|
|
719
961
|
assertFiniteNumber(year, 'year')
|
|
720
962
|
const d = toPlainDate(date)
|
|
721
|
-
return guardRange('setYear', () => toDayString(
|
|
963
|
+
return guardRange('setYear', () => toDayString(PlainDateFns.withFields(d, { year })))
|
|
722
964
|
}
|
|
723
965
|
|
|
724
966
|
/**
|
|
@@ -732,7 +974,7 @@ export function setMonth(date, month) {
|
|
|
732
974
|
throw new RangeError('daymath: month must be 1…12 (1=January)')
|
|
733
975
|
}
|
|
734
976
|
const d = toPlainDate(date)
|
|
735
|
-
return guardRange('setMonth', () => toDayString(
|
|
977
|
+
return guardRange('setMonth', () => toDayString(PlainDateFns.withFields(d, { month })))
|
|
736
978
|
}
|
|
737
979
|
|
|
738
980
|
/**
|
|
@@ -744,7 +986,9 @@ export function setMonth(date, month) {
|
|
|
744
986
|
export function setDate(date, dayOfMonth) {
|
|
745
987
|
assertFiniteNumber(dayOfMonth, 'day')
|
|
746
988
|
const d = toPlainDate(date)
|
|
747
|
-
return guardRange('setDate', () =>
|
|
989
|
+
return guardRange('setDate', () =>
|
|
990
|
+
toDayString(PlainDateFns.withFields(d, { day: dayOfMonth })),
|
|
991
|
+
)
|
|
748
992
|
}
|
|
749
993
|
|
|
750
994
|
// ─── start / end of unit ───────────────────────────────────────────
|
|
@@ -756,41 +1000,53 @@ export function setDate(date, dayOfMonth) {
|
|
|
756
1000
|
/** @param {DayInput} date @returns {string} */
|
|
757
1001
|
export function startOfMonth(date) {
|
|
758
1002
|
const d = toPlainDate(date)
|
|
759
|
-
return guardRange('startOfMonth', () =>
|
|
1003
|
+
return guardRange('startOfMonth', () =>
|
|
1004
|
+
toDayString(PlainDateFns.withFields(d, { day: 1 })),
|
|
1005
|
+
)
|
|
760
1006
|
}
|
|
761
1007
|
|
|
762
1008
|
/** @param {DayInput} date @returns {string} */
|
|
763
1009
|
export function endOfMonth(date) {
|
|
764
1010
|
const d = toPlainDate(date)
|
|
765
|
-
return guardRange('endOfMonth', () =>
|
|
1011
|
+
return guardRange('endOfMonth', () =>
|
|
1012
|
+
toDayString(PlainDateFns.withFields(d, { day: PlainDateFns.daysInMonth(d) })),
|
|
1013
|
+
)
|
|
766
1014
|
}
|
|
767
1015
|
|
|
768
1016
|
/** @param {DayInput} date @returns {string} */
|
|
769
1017
|
export function startOfYear(date) {
|
|
770
1018
|
const d = toPlainDate(date)
|
|
771
|
-
return guardRange('startOfYear', () =>
|
|
1019
|
+
return guardRange('startOfYear', () =>
|
|
1020
|
+
toDayString(PlainDateFns.withFields(d, { month: 1, day: 1 })),
|
|
1021
|
+
)
|
|
772
1022
|
}
|
|
773
1023
|
|
|
774
1024
|
/** @param {DayInput} date @returns {string} */
|
|
775
1025
|
export function endOfYear(date) {
|
|
776
1026
|
const d = toPlainDate(date)
|
|
777
|
-
return guardRange('endOfYear', () =>
|
|
1027
|
+
return guardRange('endOfYear', () =>
|
|
1028
|
+
toDayString(PlainDateFns.withFields(d, { month: 12, day: 31 })),
|
|
1029
|
+
)
|
|
778
1030
|
}
|
|
779
1031
|
|
|
780
1032
|
/** @param {DayInput} date @returns {string} */
|
|
781
1033
|
export function startOfQuarter(date) {
|
|
782
1034
|
const d = toPlainDate(date)
|
|
783
|
-
const month = (
|
|
784
|
-
return guardRange('startOfQuarter', () =>
|
|
1035
|
+
const month = (quarterOf(d) - 1) * 3 + 1
|
|
1036
|
+
return guardRange('startOfQuarter', () =>
|
|
1037
|
+
toDayString(PlainDateFns.withFields(d, { month, day: 1 })),
|
|
1038
|
+
)
|
|
785
1039
|
}
|
|
786
1040
|
|
|
787
1041
|
/** @param {DayInput} date @returns {string} */
|
|
788
1042
|
export function endOfQuarter(date) {
|
|
789
1043
|
const d = toPlainDate(date)
|
|
790
|
-
const month =
|
|
1044
|
+
const month = quarterOf(d) * 3
|
|
791
1045
|
return guardRange('endOfQuarter', () => {
|
|
792
|
-
const mid =
|
|
793
|
-
return toDayString(
|
|
1046
|
+
const mid = PlainDateFns.withFields(d, { month, day: 1 })
|
|
1047
|
+
return toDayString(
|
|
1048
|
+
PlainDateFns.withFields(mid, { day: PlainDateFns.daysInMonth(mid) }),
|
|
1049
|
+
)
|
|
794
1050
|
})
|
|
795
1051
|
}
|
|
796
1052
|
|
|
@@ -802,19 +1058,19 @@ export function endOfQuarter(date) {
|
|
|
802
1058
|
export function startOfWeek(date, options) {
|
|
803
1059
|
const d = toPlainDate(date)
|
|
804
1060
|
const diff = daysIntoWeek(d, options)
|
|
805
|
-
return guardRange('startOfWeek', () => toDayString(
|
|
1061
|
+
return guardRange('startOfWeek', () => toDayString(PlainDateFns.subtractDays(d, diff)))
|
|
806
1062
|
}
|
|
807
1063
|
|
|
808
1064
|
/**
|
|
809
1065
|
* How far the day sits past the start of its week.
|
|
810
|
-
* @param {
|
|
1066
|
+
* @param {PlainDateRecord} d
|
|
811
1067
|
* @param {WeekOptions} [options]
|
|
812
1068
|
* @returns {number}
|
|
813
1069
|
*/
|
|
814
1070
|
function daysIntoWeek(d, options) {
|
|
815
1071
|
const weekStartsOn = weekStartsOnFrom(options)
|
|
816
1072
|
// mod 7 makes weekStartsOn 0 and 7 identical, so both spellings of Sunday work
|
|
817
|
-
return (
|
|
1073
|
+
return (PlainDateFns.dayOfWeek(d) - weekStartsOn + 7) % 7
|
|
818
1074
|
}
|
|
819
1075
|
|
|
820
1076
|
/**
|
|
@@ -827,7 +1083,7 @@ export function endOfWeek(date, options) {
|
|
|
827
1083
|
const diff = daysIntoWeek(d, options)
|
|
828
1084
|
// one guard for the whole walk, so a failure at either end says endOfWeek
|
|
829
1085
|
return guardRange('endOfWeek', () =>
|
|
830
|
-
toDayString(
|
|
1086
|
+
toDayString(PlainDateFns.addDays(PlainDateFns.subtractDays(d, diff), 6)),
|
|
831
1087
|
)
|
|
832
1088
|
}
|
|
833
1089
|
|
|
@@ -842,7 +1098,26 @@ export function endOfWeek(date, options) {
|
|
|
842
1098
|
export function differenceInDays(dateLeft, dateRight) {
|
|
843
1099
|
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
844
1100
|
const right = toPlainDate(dateRight, 'dateRight')
|
|
845
|
-
|
|
1101
|
+
// `diff` IS Temporal: it is the `fns` spelling of `PlainDate.prototype.until`, which is what
|
|
1102
|
+
// `since` was here with the operands the other way round. The choice below is between two
|
|
1103
|
+
// Temporal functions, not between Temporal and anything else.
|
|
1104
|
+
//
|
|
1105
|
+
// `diff`, NOT `diffDays`, and the reason is the one day where the two Temporal ranges disagree.
|
|
1106
|
+
//
|
|
1107
|
+
// `PlainDate`'s minimum is `-271821-04-19`, one day BELOW `PlainDateTime`'s, because midnight on
|
|
1108
|
+
// that day is out of bounds while the same day is reachable in a positive-offset zone. The
|
|
1109
|
+
// maximum is not widened, so the asymmetry is real and only the low edge has it. Measured:
|
|
1110
|
+
// `PlainDate.from('-271821-04-19').toPlainDateTime()` throws `Out-of-bounds date`, and
|
|
1111
|
+
// `-271821-04-20` does not.
|
|
1112
|
+
//
|
|
1113
|
+
// `diffDays` converts to a `PlainDateTime` and inherits that, so it throws on exactly one
|
|
1114
|
+
// operand. `diff` with `largestUnit: 'day'` does not, and answers what the class API's `since`
|
|
1115
|
+
// answered. The cross-runtime baseline caught it, because no test in the suite covers a pair
|
|
1116
|
+
// that wide; `differenceInDays` and `differenceInWeeks` both went red.
|
|
1117
|
+
//
|
|
1118
|
+
// The argument order is `(record, other)` for `other − record`, so the operands are swapped to
|
|
1119
|
+
// keep daymath's date-fns order of `dateLeft − dateRight`.
|
|
1120
|
+
return PlainDateFns.diff(isoOf(right), isoOf(left), { largestUnit: 'day' }).days
|
|
846
1121
|
}
|
|
847
1122
|
|
|
848
1123
|
/**
|
|
@@ -871,18 +1146,22 @@ export function differenceInWeeks(dateLeft, dateRight) {
|
|
|
871
1146
|
* @returns {number}
|
|
872
1147
|
*/
|
|
873
1148
|
export function differenceInMonths(dateLeft, dateRight) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1149
|
+
// `isoOf` on both, like differenceInDays. Without it this function read TWO coordinate systems
|
|
1150
|
+
// at once: the sign came from `compare`, which ignores the calendar, and the magnitude came from
|
|
1151
|
+
// `differenceInCalendarMonths`, which does not. A mixed pair then measured 6,513 months for a
|
|
1152
|
+
// 29-day gap, and two days 543 ISO years apart measured 0.
|
|
1153
|
+
const left = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1154
|
+
const right = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1155
|
+
const sign = PlainDateFns.compare(left, right)
|
|
877
1156
|
if (sign === 0) return 0
|
|
878
|
-
const diff = Math.abs(
|
|
1157
|
+
const diff = Math.abs(calendarMonthsBetween(left, right))
|
|
879
1158
|
if (diff < 1) return 0
|
|
880
1159
|
const [earlier, later] = sign > 0 ? [right, left] : [left, right]
|
|
881
1160
|
// Where `earlier` lands after `diff` months: same year-month as `later` by
|
|
882
1161
|
// construction, on `earlier`'s day clamped to that month's length. Compared
|
|
883
1162
|
// as day numbers rather than built as a date, because the landing can sit
|
|
884
1163
|
// past the maximum PlainDate even when both operands are inside the range.
|
|
885
|
-
const landingDay = Math.min(earlier.day,
|
|
1164
|
+
const landingDay = Math.min(earlier.day, PlainDateFns.daysInMonth(later))
|
|
886
1165
|
const isLastMonthNotFull = landingDay > later.day
|
|
887
1166
|
return sign * (diff - +isLastMonthNotFull) || 0
|
|
888
1167
|
}
|
|
@@ -894,9 +1173,9 @@ export function differenceInMonths(dateLeft, dateRight) {
|
|
|
894
1173
|
* @returns {number}
|
|
895
1174
|
*/
|
|
896
1175
|
export function differenceInCalendarMonths(dateLeft, dateRight) {
|
|
897
|
-
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
898
|
-
const right = toPlainDate(dateRight, 'dateRight')
|
|
899
|
-
return (left
|
|
1176
|
+
const left = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1177
|
+
const right = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1178
|
+
return calendarMonthsBetween(left, right)
|
|
900
1179
|
}
|
|
901
1180
|
|
|
902
1181
|
/**
|
|
@@ -910,9 +1189,10 @@ export function differenceInCalendarMonths(dateLeft, dateRight) {
|
|
|
910
1189
|
* @returns {number}
|
|
911
1190
|
*/
|
|
912
1191
|
export function differenceInYears(dateLeft, dateRight) {
|
|
913
|
-
|
|
914
|
-
const
|
|
915
|
-
const
|
|
1192
|
+
// `isoOf` on both: a measurement, so it ignores the year LABEL. See differenceInMonths.
|
|
1193
|
+
const left = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1194
|
+
const right = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1195
|
+
const sign = PlainDateFns.compare(left, right)
|
|
916
1196
|
if (sign === 0) return 0
|
|
917
1197
|
const diff = Math.abs(left.year - right.year)
|
|
918
1198
|
if (diff < 1) return 0
|
|
@@ -923,7 +1203,9 @@ export function differenceInYears(dateLeft, dateRight) {
|
|
|
923
1203
|
// Compared field by field rather than built as a date, because the landing
|
|
924
1204
|
// can sit past the maximum PlainDate even with both operands inside the range.
|
|
925
1205
|
const landingDay =
|
|
926
|
-
earlier.month === 2 && earlier.day === 29 && !
|
|
1206
|
+
earlier.month === 2 && earlier.day === 29 && !PlainDateFns.inLeapYear(later)
|
|
1207
|
+
? 28
|
|
1208
|
+
: earlier.day
|
|
927
1209
|
const isLastYearNotFull =
|
|
928
1210
|
earlier.month > later.month ||
|
|
929
1211
|
(earlier.month === later.month && landingDay > later.day)
|
|
@@ -937,7 +1219,12 @@ export function differenceInYears(dateLeft, dateRight) {
|
|
|
937
1219
|
* @returns {number}
|
|
938
1220
|
*/
|
|
939
1221
|
export function differenceInCalendarYears(dateLeft, dateRight) {
|
|
940
|
-
|
|
1222
|
+
// NOT getYear: that is a field read and honours the label, so a mixed pair subtracted two
|
|
1223
|
+
// different year spaces and answered 0 for days 543 ISO years apart.
|
|
1224
|
+
return (
|
|
1225
|
+
isoOf(toPlainDate(dateLeft, 'dateLeft')).year -
|
|
1226
|
+
isoOf(toPlainDate(dateRight, 'dateRight')).year
|
|
1227
|
+
)
|
|
941
1228
|
}
|
|
942
1229
|
|
|
943
1230
|
/**
|
|
@@ -959,9 +1246,9 @@ export function differenceInQuarters(dateLeft, dateRight) {
|
|
|
959
1246
|
* @returns {number}
|
|
960
1247
|
*/
|
|
961
1248
|
export function differenceInCalendarQuarters(dateLeft, dateRight) {
|
|
962
|
-
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
963
|
-
const right = toPlainDate(dateRight, 'dateRight')
|
|
964
|
-
return (left.year - right.year) * 4 + (
|
|
1249
|
+
const left = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1250
|
+
const right = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1251
|
+
return (left.year - right.year) * 4 + (quarterOf(left) - quarterOf(right))
|
|
965
1252
|
}
|
|
966
1253
|
|
|
967
1254
|
// ─── compare / equal ───────────────────────────────────────────────
|
|
@@ -973,10 +1260,8 @@ export function differenceInCalendarQuarters(dateLeft, dateRight) {
|
|
|
973
1260
|
*/
|
|
974
1261
|
export function isBefore(date, dateToCompare) {
|
|
975
1262
|
return (
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
toPlainDate(dateToCompare, 'dateToCompare'),
|
|
979
|
-
) < 0
|
|
1263
|
+
PlainDateFns.compare(toPlainDate(date), toPlainDate(dateToCompare, 'dateToCompare')) <
|
|
1264
|
+
0
|
|
980
1265
|
)
|
|
981
1266
|
}
|
|
982
1267
|
|
|
@@ -987,10 +1272,8 @@ export function isBefore(date, dateToCompare) {
|
|
|
987
1272
|
*/
|
|
988
1273
|
export function isAfter(date, dateToCompare) {
|
|
989
1274
|
return (
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
toPlainDate(dateToCompare, 'dateToCompare'),
|
|
993
|
-
) > 0
|
|
1275
|
+
PlainDateFns.compare(toPlainDate(date), toPlainDate(dateToCompare, 'dateToCompare')) >
|
|
1276
|
+
0
|
|
994
1277
|
)
|
|
995
1278
|
}
|
|
996
1279
|
|
|
@@ -1002,7 +1285,7 @@ export function isAfter(date, dateToCompare) {
|
|
|
1002
1285
|
*/
|
|
1003
1286
|
export function isEqual(dateLeft, dateRight) {
|
|
1004
1287
|
return (
|
|
1005
|
-
|
|
1288
|
+
PlainDateFns.compare(
|
|
1006
1289
|
toPlainDate(dateLeft, 'dateLeft'),
|
|
1007
1290
|
toPlainDate(dateRight, 'dateRight'),
|
|
1008
1291
|
) === 0
|
|
@@ -1022,10 +1305,15 @@ export function isSameWeek(dateLeft, dateRight, options) {
|
|
|
1022
1305
|
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
1023
1306
|
const right = toPlainDate(dateRight, 'dateRight')
|
|
1024
1307
|
// own guard, so a week start below the minimum does not say startOfWeek
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1308
|
+
// compare, not equals: Temporal's equals compares the calendar as well as the day, so it
|
|
1309
|
+
// answers false for the same week in two calendars. compare reads the ISO fields alone.
|
|
1310
|
+
return guardRange(
|
|
1311
|
+
'isSameWeek',
|
|
1312
|
+
() =>
|
|
1313
|
+
PlainDateFns.compare(
|
|
1314
|
+
PlainDateFns.subtractDays(left, daysIntoWeek(left, options)),
|
|
1315
|
+
PlainDateFns.subtractDays(right, daysIntoWeek(right, options)),
|
|
1316
|
+
) === 0,
|
|
1029
1317
|
)
|
|
1030
1318
|
}
|
|
1031
1319
|
|
|
@@ -1035,8 +1323,8 @@ export function isSameWeek(dateLeft, dateRight, options) {
|
|
|
1035
1323
|
* @returns {boolean}
|
|
1036
1324
|
*/
|
|
1037
1325
|
export function isSameMonth(dateLeft, dateRight) {
|
|
1038
|
-
const a = toPlainDate(dateLeft, 'dateLeft')
|
|
1039
|
-
const b = toPlainDate(dateRight, 'dateRight')
|
|
1326
|
+
const a = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1327
|
+
const b = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1040
1328
|
return a.year === b.year && a.month === b.month
|
|
1041
1329
|
}
|
|
1042
1330
|
|
|
@@ -1046,7 +1334,12 @@ export function isSameMonth(dateLeft, dateRight) {
|
|
|
1046
1334
|
* @returns {boolean}
|
|
1047
1335
|
*/
|
|
1048
1336
|
export function isSameYear(dateLeft, dateRight) {
|
|
1049
|
-
|
|
1337
|
+
// NOT getYear, for the same reason as differenceInCalendarYears: two days 543 ISO years apart
|
|
1338
|
+
// both read year 2569 once one of them carries a Buddhist label, and this answered true.
|
|
1339
|
+
return (
|
|
1340
|
+
isoOf(toPlainDate(dateLeft, 'dateLeft')).year ===
|
|
1341
|
+
isoOf(toPlainDate(dateRight, 'dateRight')).year
|
|
1342
|
+
)
|
|
1050
1343
|
}
|
|
1051
1344
|
|
|
1052
1345
|
/**
|
|
@@ -1055,9 +1348,9 @@ export function isSameYear(dateLeft, dateRight) {
|
|
|
1055
1348
|
* @returns {boolean}
|
|
1056
1349
|
*/
|
|
1057
1350
|
export function isSameQuarter(dateLeft, dateRight) {
|
|
1058
|
-
const a = toPlainDate(dateLeft, 'dateLeft')
|
|
1059
|
-
const b = toPlainDate(dateRight, 'dateRight')
|
|
1060
|
-
return a.year === b.year &&
|
|
1351
|
+
const a = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1352
|
+
const b = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1353
|
+
return a.year === b.year && quarterOf(a) === quarterOf(b)
|
|
1061
1354
|
}
|
|
1062
1355
|
|
|
1063
1356
|
/**
|
|
@@ -1067,7 +1360,7 @@ export function isSameQuarter(dateLeft, dateRight) {
|
|
|
1067
1360
|
*/
|
|
1068
1361
|
export function compareAsc(dateLeft, dateRight) {
|
|
1069
1362
|
return /** @type {-1 | 0 | 1} */ (
|
|
1070
|
-
|
|
1363
|
+
PlainDateFns.compare(
|
|
1071
1364
|
toPlainDate(dateLeft, 'dateLeft'),
|
|
1072
1365
|
toPlainDate(dateRight, 'dateRight'),
|
|
1073
1366
|
)
|
|
@@ -1093,7 +1386,7 @@ export function min(dates) {
|
|
|
1093
1386
|
return toDayString(
|
|
1094
1387
|
dates
|
|
1095
1388
|
.map((d) => toPlainDate(d))
|
|
1096
|
-
.reduce((a, b) => (
|
|
1389
|
+
.reduce((a, b) => (PlainDateFns.compare(a, b) <= 0 ? a : b)),
|
|
1097
1390
|
)
|
|
1098
1391
|
}
|
|
1099
1392
|
|
|
@@ -1106,7 +1399,7 @@ export function max(dates) {
|
|
|
1106
1399
|
return toDayString(
|
|
1107
1400
|
dates
|
|
1108
1401
|
.map((d) => toPlainDate(d))
|
|
1109
|
-
.reduce((a, b) => (
|
|
1402
|
+
.reduce((a, b) => (PlainDateFns.compare(a, b) >= 0 ? a : b)),
|
|
1110
1403
|
)
|
|
1111
1404
|
}
|
|
1112
1405
|
|
|
@@ -1154,7 +1447,7 @@ export function isFirstDayOfMonth(date) {
|
|
|
1154
1447
|
/** @param {DayInput} date @returns {boolean} */
|
|
1155
1448
|
export function isLastDayOfMonth(date) {
|
|
1156
1449
|
const d = toPlainDate(date)
|
|
1157
|
-
return d.day ===
|
|
1450
|
+
return d.day === PlainDateFns.daysInMonth(d)
|
|
1158
1451
|
}
|
|
1159
1452
|
|
|
1160
1453
|
// ─── intervals ─────────────────────────────────────────────────────
|
|
@@ -1166,7 +1459,7 @@ export function isLastDayOfMonth(date) {
|
|
|
1166
1459
|
*/
|
|
1167
1460
|
export function eachDayOfInterval(interval) {
|
|
1168
1461
|
const { start, end } = toInterval(interval)
|
|
1169
|
-
if (
|
|
1462
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1170
1463
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1171
1464
|
}
|
|
1172
1465
|
/** @type {string[]} */
|
|
@@ -1176,8 +1469,8 @@ export function eachDayOfInterval(interval) {
|
|
|
1176
1469
|
// PlainDate (+275760-09-13) throws
|
|
1177
1470
|
for (;;) {
|
|
1178
1471
|
out.push(toDayString(cur))
|
|
1179
|
-
if (
|
|
1180
|
-
cur =
|
|
1472
|
+
if (PlainDateFns.compare(cur, end) >= 0) break
|
|
1473
|
+
cur = PlainDateFns.addDays(cur, 1)
|
|
1181
1474
|
}
|
|
1182
1475
|
return out
|
|
1183
1476
|
}
|
|
@@ -1189,22 +1482,22 @@ export function eachDayOfInterval(interval) {
|
|
|
1189
1482
|
*/
|
|
1190
1483
|
export function eachMonthOfInterval(interval) {
|
|
1191
1484
|
const { start, end } = toInterval(interval)
|
|
1192
|
-
if (
|
|
1485
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1193
1486
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1194
1487
|
}
|
|
1195
1488
|
/** @type {string[]} */
|
|
1196
1489
|
const out = []
|
|
1197
1490
|
// the 1st of start's month can sit below the minimum PlainDate
|
|
1198
1491
|
const [cur0, last] = guardRange('eachMonthOfInterval', () => [
|
|
1199
|
-
|
|
1200
|
-
|
|
1492
|
+
PlainDateFns.withFields(start, { day: 1 }),
|
|
1493
|
+
PlainDateFns.withFields(end, { day: 1 }),
|
|
1201
1494
|
])
|
|
1202
1495
|
let cur = cur0
|
|
1203
1496
|
// same boundary rule as eachDayOfInterval
|
|
1204
1497
|
for (;;) {
|
|
1205
1498
|
out.push(toDayString(cur))
|
|
1206
|
-
if (
|
|
1207
|
-
cur =
|
|
1499
|
+
if (PlainDateFns.compare(cur, last) >= 0) break
|
|
1500
|
+
cur = PlainDateFns.addMonths(cur, 1)
|
|
1208
1501
|
}
|
|
1209
1502
|
return out
|
|
1210
1503
|
}
|
|
@@ -1216,17 +1509,23 @@ export function eachMonthOfInterval(interval) {
|
|
|
1216
1509
|
*/
|
|
1217
1510
|
export function eachYearOfInterval(interval) {
|
|
1218
1511
|
const { start, end } = toInterval(interval)
|
|
1219
|
-
if (
|
|
1512
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1220
1513
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1221
1514
|
}
|
|
1222
1515
|
/** @type {string[]} */
|
|
1223
1516
|
const out = []
|
|
1224
|
-
|
|
1225
|
-
//
|
|
1517
|
+
// `with` then `add`, never `PlainDate.from({year})`: both keep start's calendar, and the object
|
|
1518
|
+
// form would read `year` as an ISO year, which is 543 years out for a Buddhist day.
|
|
1519
|
+
// Jan 1 of start's year can sit below the minimum PlainDate.
|
|
1226
1520
|
guardRange('eachYearOfInterval', () => {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1521
|
+
// Compare ISO years, then add. Testing the loop condition AFTER the push is what keeps the
|
|
1522
|
+
// top edge working: Jan 1 of +275760 is valid, and adding a year to it is not.
|
|
1523
|
+
const lastIsoYear = isoOf(end).year
|
|
1524
|
+
let cur = PlainDateFns.withFields(start, { month: 1, day: 1 })
|
|
1525
|
+
for (;;) {
|
|
1526
|
+
out.push(toDayString(cur))
|
|
1527
|
+
if (isoOf(cur).year >= lastIsoYear) break
|
|
1528
|
+
cur = PlainDateFns.addYears(cur, 1)
|
|
1230
1529
|
}
|
|
1231
1530
|
})
|
|
1232
1531
|
return out
|
|
@@ -1241,12 +1540,10 @@ export function eachYearOfInterval(interval) {
|
|
|
1241
1540
|
export function isWithinInterval(date, interval) {
|
|
1242
1541
|
const d = toPlainDate(date)
|
|
1243
1542
|
const { start, end } = toInterval(interval)
|
|
1244
|
-
if (
|
|
1543
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1245
1544
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1246
1545
|
}
|
|
1247
|
-
return (
|
|
1248
|
-
Temporal.PlainDate.compare(d, start) >= 0 && Temporal.PlainDate.compare(d, end) <= 0
|
|
1249
|
-
)
|
|
1546
|
+
return PlainDateFns.compare(d, start) >= 0 && PlainDateFns.compare(d, end) <= 0
|
|
1250
1547
|
}
|
|
1251
1548
|
|
|
1252
1549
|
/**
|
|
@@ -1258,11 +1555,11 @@ export function isWithinInterval(date, interval) {
|
|
|
1258
1555
|
export function clamp(date, interval) {
|
|
1259
1556
|
const d = toPlainDate(date)
|
|
1260
1557
|
const { start, end } = toInterval(interval)
|
|
1261
|
-
if (
|
|
1558
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1262
1559
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1263
1560
|
}
|
|
1264
|
-
if (
|
|
1265
|
-
if (
|
|
1561
|
+
if (PlainDateFns.compare(d, start) < 0) return toDayString(start)
|
|
1562
|
+
if (PlainDateFns.compare(d, end) > 0) return toDayString(end)
|
|
1266
1563
|
return toDayString(d)
|
|
1267
1564
|
}
|
|
1268
1565
|
|
|
@@ -1276,22 +1573,21 @@ export function clamp(date, interval) {
|
|
|
1276
1573
|
export function areIntervalsOverlapping(intervalLeft, intervalRight, options) {
|
|
1277
1574
|
const a = toInterval(intervalLeft)
|
|
1278
1575
|
const b = toInterval(intervalRight)
|
|
1279
|
-
if (
|
|
1576
|
+
if (PlainDateFns.compare(a.start, a.end) > 0) {
|
|
1280
1577
|
throw new RangeError('daymath: intervalLeft start must not be after end')
|
|
1281
1578
|
}
|
|
1282
|
-
if (
|
|
1579
|
+
if (PlainDateFns.compare(b.start, b.end) > 0) {
|
|
1283
1580
|
throw new RangeError('daymath: intervalRight start must not be after end')
|
|
1284
1581
|
}
|
|
1285
1582
|
const inclusive = options?.inclusive ?? false
|
|
1286
1583
|
if (inclusive) {
|
|
1287
1584
|
return (
|
|
1288
|
-
|
|
1289
|
-
|
|
1585
|
+
PlainDateFns.compare(a.start, b.end) <= 0 &&
|
|
1586
|
+
PlainDateFns.compare(b.start, a.end) <= 0
|
|
1290
1587
|
)
|
|
1291
1588
|
}
|
|
1292
1589
|
// date-fns default: touch-at-endpoint is NOT overlap
|
|
1293
1590
|
return (
|
|
1294
|
-
|
|
1295
|
-
Temporal.PlainDate.compare(b.start, a.end) < 0
|
|
1591
|
+
PlainDateFns.compare(a.start, b.end) < 0 && PlainDateFns.compare(b.start, a.end) < 0
|
|
1296
1592
|
)
|
|
1297
1593
|
}
|