daymath 0.5.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 +10 -5
- package/index.js +227 -185
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,8 +155,11 @@ Amounts are finite integers.
|
|
|
155
155
|
|
|
156
156
|
## Temporal
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
Built on [`temporal-polyfill/fns`](https://www.npmjs.com/package/temporal-polyfill), the functional
|
|
159
|
+
API, rather than the `Temporal` class. A class is one unit to a bundler, so the class build shipped
|
|
160
|
+
whole for the twenty-odd operations daymath uses; free functions drop what you do not call. It runs
|
|
161
|
+
on native `Temporal` where the runtime has it and on the bundled build elsewhere, and `fns` makes
|
|
162
|
+
that choice itself. Measured on a three-call program: **24.7 kB gzip to 16.3 kB, −34%.**
|
|
160
163
|
|
|
161
164
|
A `Temporal.PlainDate` from *any* implementation is accepted — native, the bundled polyfill,
|
|
162
165
|
or a second copy of it in the same dependency tree. daymath reads its ISO day and builds its
|
|
@@ -312,9 +315,11 @@ Temporal.PlainDate.from({year: 2569, month: 8, day: 8, calendar: 'buddhist'}).to
|
|
|
312
315
|
One more trap in the same family: `'2569-08-08[u-ca=buddhist]'` is a valid string, and its
|
|
313
316
|
`getYear` is **3112**. The date part is ISO 2569, and the annotation adds 543 on top.
|
|
314
317
|
|
|
315
|
-
Supporting these calendars
|
|
316
|
-
|
|
317
|
-
|
|
318
|
+
Supporting these calendars costs **4.2 kB gzip**, because daymath resolves them with `getAny`, the
|
|
319
|
+
`fns` resolver that carries every calendar's data. The narrower resolvers cannot serve the rule: on
|
|
320
|
+
a runtime without native `Temporal` they drop the annotation instead of refusing it, so the same
|
|
321
|
+
program would answer 2569 on one lane and 2026 on another. `getAny` answers identically everywhere,
|
|
322
|
+
which is the point.
|
|
318
323
|
|
|
319
324
|
Error messages quote no Temporal text, because implementations word the same failure
|
|
320
325
|
differently. The original error is on `cause`.
|
package/index.js
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
/** daymath — calendar date math (ISO 8601 day). date-fns-shaped. No Date. No time zones. */
|
|
2
|
-
//
|
|
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.
|
|
3
7
|
//
|
|
4
|
-
// `
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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.
|
|
9
15
|
//
|
|
10
|
-
//
|
|
11
|
-
// `
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// rather than naming the capability loss. Import order decided it, which is worse than a wrong
|
|
15
|
-
// answer. So the candidate is PROBED, not trusted.
|
|
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.
|
|
16
20
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// cannot prove that if daymath runs the polyfill there. And an engine-level Temporal fix then
|
|
29
|
-
// reaches callers with no release from here.
|
|
30
|
-
import { Temporal as bundledTemporal } from 'temporal-polyfill/full'
|
|
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')
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Every calendar this runtime names. Read once, lowercased, because BCP-47 keys are
|
|
@@ -42,55 +43,6 @@ const RUNTIME_CALENDARS = new Set(
|
|
|
42
43
|
Intl.supportedValuesOf('calendar').map((id) => id.toLowerCase()),
|
|
43
44
|
)
|
|
44
45
|
|
|
45
|
-
/**
|
|
46
|
-
* Can this Temporal build a calendar beyond the two every build has?
|
|
47
|
-
*
|
|
48
|
-
* The cast is needed because `Temporal` is not declared on `globalThis` in the type space, and
|
|
49
|
-
* daymath deliberately installs no global of its own.
|
|
50
|
-
* @param {unknown} candidate
|
|
51
|
-
* @returns {candidate is typeof bundledTemporal}
|
|
52
|
-
*/
|
|
53
|
-
/*
|
|
54
|
-
* Every branch here turns on `globalThis.Temporal`, which is read once at module load. A test in
|
|
55
|
-
* this process cannot change it after the fact, so none of these branches is reachable from the
|
|
56
|
-
* suite. They are covered three other ways, and each is real:
|
|
57
|
-
*
|
|
58
|
-
* 1. `test.js` spawns a CHILD process that installs the BASE polyfill global and then imports
|
|
59
|
-
* daymath. That is the exact scenario this function exists for, and it asserts the four
|
|
60
|
-
* calendars survive. A child's execution does not count toward this file's coverage.
|
|
61
|
-
* 2. The Node 20 and bun CI lanes have no native Temporal, so they take the fallback for real.
|
|
62
|
-
* 3. `npm run test:runtimes` proves every lane answers identically.
|
|
63
|
-
*/
|
|
64
|
-
/* c8 ignore start */
|
|
65
|
-
function buildsExoticCalendars(candidate) {
|
|
66
|
-
const temporal = /** @type {typeof bundledTemporal | undefined} */ (candidate)
|
|
67
|
-
if (temporal?.PlainDate?.from === undefined) return false
|
|
68
|
-
// Ask the runtime for a calendar rather than naming one. The two excluded here are the pair every
|
|
69
|
-
// build can construct, so anything else proves the exotic calendar data is present.
|
|
70
|
-
const exotic = [...RUNTIME_CALENDARS].find((id) => id !== 'iso8601' && id !== 'gregory')
|
|
71
|
-
if (exotic === undefined) return true // nothing to prove it against
|
|
72
|
-
try {
|
|
73
|
-
temporal.PlainDate.from('2026-01-31').withCalendar(exotic)
|
|
74
|
-
return true
|
|
75
|
-
} catch {
|
|
76
|
-
return false
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/* c8 ignore stop */
|
|
80
|
-
|
|
81
|
-
// The fallback needs a runtime whose global Temporal is absent or not calendar-capable, so a process
|
|
82
|
-
// with native Temporal cannot reach it. It is covered for real by the Node 20 and bun CI lanes, and
|
|
83
|
-
// `npm run test:runtimes` proves those lanes answer identically to the native ones.
|
|
84
|
-
// One cast, because `Temporal` is not declared on `globalThis` in the type space and daymath
|
|
85
|
-
// deliberately installs no global of its own.
|
|
86
|
-
const globalTemporal = /** @type {{Temporal?: unknown}} */ (globalThis).Temporal
|
|
87
|
-
|
|
88
|
-
// The fallback needs a runtime whose global Temporal is absent or not calendar-capable, so a process
|
|
89
|
-
// with native Temporal cannot reach it. It is covered for real by the Node 20 and bun CI lanes, and
|
|
90
|
-
// `npm run test:runtimes` proves those lanes answer identically to the native ones.
|
|
91
|
-
/* c8 ignore next */
|
|
92
|
-
const Temporal = buildsExoticCalendars(globalTemporal) ? globalTemporal : bundledTemporal
|
|
93
|
-
|
|
94
46
|
/**
|
|
95
47
|
* ISO 8601 calendar day string:
|
|
96
48
|
* - `YYYY-MM-DD` (years 0000–9999)
|
|
@@ -169,9 +121,12 @@ function hasZoneAnnotation(text) {
|
|
|
169
121
|
const ZONE_LIKE =
|
|
170
122
|
/^(?:(?![Tt]\d)[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)*|[+-]\d{2}(?::?\d{2})?)$/u
|
|
171
123
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
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 */
|
|
175
130
|
/** @typedef {import('temporal-polyfill').Temporal.PlainDate} PlainDate */
|
|
176
131
|
/** @typedef {string | PlainDate} DayInput */
|
|
177
132
|
/**
|
|
@@ -281,9 +236,12 @@ function calendarRule(calendar) {
|
|
|
281
236
|
let verdict = { reason: 'renumbers' }
|
|
282
237
|
try {
|
|
283
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)
|
|
284
242
|
for (const probe of CALENDAR_PROBES) {
|
|
285
|
-
const iso =
|
|
286
|
-
const dated =
|
|
243
|
+
const iso = PlainDateFns.fromString(probe, getAny)
|
|
244
|
+
const dated = PlainDateFns.withCalendar(iso, calendarRecord)
|
|
287
245
|
if (dated.month !== iso.month || dated.day !== iso.day) {
|
|
288
246
|
offsets.clear()
|
|
289
247
|
break
|
|
@@ -363,7 +321,7 @@ function isPlainDate(value) {
|
|
|
363
321
|
* reasons live on `supportedCalendar`; this function only applies the verdict.
|
|
364
322
|
* @param {unknown} value
|
|
365
323
|
* @param {string} label
|
|
366
|
-
* @returns {
|
|
324
|
+
* @returns {PlainDateRecord}
|
|
367
325
|
*/
|
|
368
326
|
function toPlainDate(value, label = 'date') {
|
|
369
327
|
if (value instanceof Date) {
|
|
@@ -387,10 +345,15 @@ function toPlainDate(value, label = 'date') {
|
|
|
387
345
|
)
|
|
388
346
|
}
|
|
389
347
|
try {
|
|
390
|
-
|
|
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)
|
|
391
352
|
// The annotation rides along, so getYear answers 2569 for a Buddhist day and every
|
|
392
353
|
// returned string keeps the calendar the caller named.
|
|
393
|
-
return calendar === undefined
|
|
354
|
+
return calendar === undefined
|
|
355
|
+
? plain
|
|
356
|
+
: PlainDateFns.withCalendar(plain, getAny(calendar))
|
|
394
357
|
} catch (err) {
|
|
395
358
|
throw new RangeError(`daymath: invalid ${label} ${JSON.stringify(text)}`, {
|
|
396
359
|
cause: err,
|
|
@@ -405,16 +368,18 @@ function toPlainDate(value, label = 'date') {
|
|
|
405
368
|
* `equals` compares the calendar as well as the day. Neither matters to a day count: a day is the
|
|
406
369
|
* same day whatever the year is labelled, so daymath normalises and answers. Only the exports that
|
|
407
370
|
* *read* or *write* a field honour the label.
|
|
408
|
-
* @param {
|
|
409
|
-
* @returns {
|
|
371
|
+
* @param {PlainDateRecord} plain
|
|
372
|
+
* @returns {PlainDateRecord}
|
|
410
373
|
*/
|
|
411
374
|
function isoOf(plain) {
|
|
412
|
-
return plain.calendarId === 'iso8601'
|
|
375
|
+
return plain.calendarId === 'iso8601'
|
|
376
|
+
? plain
|
|
377
|
+
: PlainDateFns.withCalendar(plain, ISO_CALENDAR)
|
|
413
378
|
}
|
|
414
379
|
|
|
415
|
-
/** @param {
|
|
380
|
+
/** @param {PlainDateRecord} plain @returns {string} */
|
|
416
381
|
function toDayString(plain) {
|
|
417
|
-
return
|
|
382
|
+
return PlainDateFns.toString(plain)
|
|
418
383
|
}
|
|
419
384
|
|
|
420
385
|
/** @param {unknown} n @param {string} label */
|
|
@@ -455,14 +420,29 @@ function guardRange(label, op) {
|
|
|
455
420
|
/**
|
|
456
421
|
* Shared body for every add/sub function. Each caller passes its own name, so
|
|
457
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.
|
|
458
431
|
* @param {DayInput} date
|
|
459
|
-
* @param {
|
|
432
|
+
* @param {'days'|'months'|'years'} unit
|
|
433
|
+
* @param {number} amount
|
|
460
434
|
* @param {string} label
|
|
461
435
|
* @returns {string}
|
|
462
436
|
*/
|
|
463
|
-
function addDuration(date,
|
|
437
|
+
function addDuration(date, unit, amount, label) {
|
|
464
438
|
const d = toPlainDate(date)
|
|
465
|
-
|
|
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)))
|
|
466
446
|
}
|
|
467
447
|
|
|
468
448
|
/** @param {unknown} dates */
|
|
@@ -486,7 +466,7 @@ function weekStartsOnFrom(options) {
|
|
|
486
466
|
|
|
487
467
|
/**
|
|
488
468
|
* @param {unknown} interval
|
|
489
|
-
* @returns {{ start:
|
|
469
|
+
* @returns {{ start: PlainDateRecord, end: PlainDateRecord }}
|
|
490
470
|
*/
|
|
491
471
|
function toInterval(interval) {
|
|
492
472
|
// `typeof x === 'object'` alone let a Date, an array or a PlainDate through,
|
|
@@ -580,9 +560,9 @@ export function day(moment, tz) {
|
|
|
580
560
|
const isMoment = isDay || moment instanceof Date || typeof moment === 'number'
|
|
581
561
|
|
|
582
562
|
let zone = tz
|
|
583
|
-
/** @type {import('temporal-polyfill').
|
|
563
|
+
/** @type {import('temporal-polyfill/fns/Instant').Record | undefined} */
|
|
584
564
|
let instant
|
|
585
|
-
/** @type {import('temporal-polyfill').
|
|
565
|
+
/** @type {import('temporal-polyfill/fns/ZonedDateTime').Record | undefined} */
|
|
586
566
|
let zoned
|
|
587
567
|
if (!isMoment && moment !== undefined && moment !== null) {
|
|
588
568
|
if (typeof moment !== 'string') {
|
|
@@ -612,7 +592,9 @@ export function day(moment, tz) {
|
|
|
612
592
|
)
|
|
613
593
|
}
|
|
614
594
|
try {
|
|
615
|
-
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)
|
|
616
598
|
} catch (err) {
|
|
617
599
|
throw new RangeError(
|
|
618
600
|
`daymath: day() could not read ${JSON.stringify(moment)} in the time zone it names`,
|
|
@@ -628,7 +610,7 @@ export function day(moment, tz) {
|
|
|
628
610
|
// year, month or day for a calendar to renumber, and without a zone
|
|
629
611
|
// bracket Temporal will not build anything that does. Refusing it would
|
|
630
612
|
// reject a right answer for a reason that cannot apply.
|
|
631
|
-
instant =
|
|
613
|
+
instant = InstantFns.fromString(moment)
|
|
632
614
|
} catch {
|
|
633
615
|
// Not a moment, so the string must be a zone — decided by shape, before
|
|
634
616
|
// Temporal sees it. Temporal's zone grammar also accepts a whole
|
|
@@ -670,10 +652,10 @@ export function day(moment, tz) {
|
|
|
670
652
|
// whatever the moment is. A caller mapping rows that are sometimes a Date and
|
|
671
653
|
// sometimes a day string would otherwise see the typo only on some rows.
|
|
672
654
|
//
|
|
673
|
-
// `ZonedDateTime.
|
|
655
|
+
// `ZonedDateTime.fromFields` accepts exactly the zone ids `Now` accepts and reads
|
|
674
656
|
// no clock, so a day input stays a pure function.
|
|
675
657
|
try {
|
|
676
|
-
|
|
658
|
+
ZonedFns.fromFields({ timeZone: zone, year: 1970, month: 1, day: 1 })
|
|
677
659
|
} catch (err) {
|
|
678
660
|
throw new RangeError(
|
|
679
661
|
`daymath: day() got an unknown time zone ${JSON.stringify(zone)}`,
|
|
@@ -698,17 +680,24 @@ export function day(moment, tz) {
|
|
|
698
680
|
// The result still goes through `toPlainDate`, so the calendar is adjudicated
|
|
699
681
|
// in one place, and then through `isoOf` for the reason above.
|
|
700
682
|
if (zoned !== undefined) {
|
|
701
|
-
|
|
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
|
+
)
|
|
702
689
|
return toDayString(isoOf(toPlainDate(plain)))
|
|
703
690
|
}
|
|
704
691
|
|
|
705
692
|
if (instant !== undefined) {
|
|
706
693
|
return guardRange('day', () =>
|
|
707
|
-
|
|
694
|
+
PlainDateFns.toString(
|
|
695
|
+
ZonedFns.toPlainDate(InstantFns.toZonedDateTimeISO(instant, zone)),
|
|
696
|
+
),
|
|
708
697
|
)
|
|
709
698
|
}
|
|
710
699
|
|
|
711
|
-
if (!isMoment) return
|
|
700
|
+
if (!isMoment) return PlainDateFns.toString(NowFns.plainDateISO(zone))
|
|
712
701
|
|
|
713
702
|
// Truncate, because `new Date(n)` truncates, and the contract here is that a
|
|
714
703
|
// number reads exactly as it does. Verified equal on positive and negative
|
|
@@ -720,10 +709,11 @@ export function day(moment, tz) {
|
|
|
720
709
|
? Math.trunc(moment)
|
|
721
710
|
: /** @type {Date} */ (moment).getTime()
|
|
722
711
|
return guardRange('day', () =>
|
|
723
|
-
|
|
724
|
-
.
|
|
725
|
-
|
|
726
|
-
|
|
712
|
+
PlainDateFns.toString(
|
|
713
|
+
ZonedFns.toPlainDate(
|
|
714
|
+
InstantFns.toZonedDateTimeISO(InstantFns.fromEpochMilliseconds(epochMs), zone),
|
|
715
|
+
),
|
|
716
|
+
),
|
|
727
717
|
)
|
|
728
718
|
}
|
|
729
719
|
|
|
@@ -783,7 +773,7 @@ export function format(date, pattern = 'yyyy-MM-dd') {
|
|
|
783
773
|
*/
|
|
784
774
|
export function addDays(date, amount) {
|
|
785
775
|
assertFiniteNumber(amount, 'amount')
|
|
786
|
-
return addDuration(date,
|
|
776
|
+
return addDuration(date, 'days', amount, 'addDays')
|
|
787
777
|
}
|
|
788
778
|
|
|
789
779
|
/**
|
|
@@ -793,7 +783,7 @@ export function addDays(date, amount) {
|
|
|
793
783
|
*/
|
|
794
784
|
export function subDays(date, amount) {
|
|
795
785
|
assertFiniteNumber(amount, 'amount')
|
|
796
|
-
return addDuration(date,
|
|
786
|
+
return addDuration(date, 'days', -amount, 'subDays')
|
|
797
787
|
}
|
|
798
788
|
|
|
799
789
|
/**
|
|
@@ -805,7 +795,7 @@ export function addWeeks(date, amount) {
|
|
|
805
795
|
assertFiniteNumber(amount, 'amount')
|
|
806
796
|
const days = amount * 7 // re-check: 7x a finite amount can still reach Infinity
|
|
807
797
|
assertFiniteNumber(days, 'amount')
|
|
808
|
-
return addDuration(date,
|
|
798
|
+
return addDuration(date, 'days', days, 'addWeeks')
|
|
809
799
|
}
|
|
810
800
|
|
|
811
801
|
/**
|
|
@@ -817,7 +807,7 @@ export function subWeeks(date, amount) {
|
|
|
817
807
|
assertFiniteNumber(amount, 'amount')
|
|
818
808
|
const days = -amount * 7
|
|
819
809
|
assertFiniteNumber(days, 'amount')
|
|
820
|
-
return addDuration(date,
|
|
810
|
+
return addDuration(date, 'days', days, 'subWeeks')
|
|
821
811
|
}
|
|
822
812
|
|
|
823
813
|
/**
|
|
@@ -828,7 +818,7 @@ export function subWeeks(date, amount) {
|
|
|
828
818
|
*/
|
|
829
819
|
export function addMonths(date, amount) {
|
|
830
820
|
assertFiniteNumber(amount, 'amount')
|
|
831
|
-
return addDuration(date,
|
|
821
|
+
return addDuration(date, 'months', amount, 'addMonths')
|
|
832
822
|
}
|
|
833
823
|
|
|
834
824
|
/**
|
|
@@ -838,7 +828,7 @@ export function addMonths(date, amount) {
|
|
|
838
828
|
*/
|
|
839
829
|
export function subMonths(date, amount) {
|
|
840
830
|
assertFiniteNumber(amount, 'amount')
|
|
841
|
-
return addDuration(date,
|
|
831
|
+
return addDuration(date, 'months', -amount, 'subMonths')
|
|
842
832
|
}
|
|
843
833
|
|
|
844
834
|
/**
|
|
@@ -848,7 +838,7 @@ export function subMonths(date, amount) {
|
|
|
848
838
|
*/
|
|
849
839
|
export function addYears(date, amount) {
|
|
850
840
|
assertFiniteNumber(amount, 'amount')
|
|
851
|
-
return addDuration(date,
|
|
841
|
+
return addDuration(date, 'years', amount, 'addYears')
|
|
852
842
|
}
|
|
853
843
|
|
|
854
844
|
/**
|
|
@@ -858,7 +848,7 @@ export function addYears(date, amount) {
|
|
|
858
848
|
*/
|
|
859
849
|
export function subYears(date, amount) {
|
|
860
850
|
assertFiniteNumber(amount, 'amount')
|
|
861
|
-
return addDuration(date,
|
|
851
|
+
return addDuration(date, 'years', -amount, 'subYears')
|
|
862
852
|
}
|
|
863
853
|
|
|
864
854
|
/**
|
|
@@ -870,7 +860,7 @@ export function addQuarters(date, amount) {
|
|
|
870
860
|
assertFiniteNumber(amount, 'amount')
|
|
871
861
|
const months = amount * 3
|
|
872
862
|
assertFiniteNumber(months, 'amount')
|
|
873
|
-
return addDuration(date,
|
|
863
|
+
return addDuration(date, 'months', months, 'addQuarters')
|
|
874
864
|
}
|
|
875
865
|
|
|
876
866
|
/**
|
|
@@ -882,7 +872,7 @@ export function subQuarters(date, amount) {
|
|
|
882
872
|
assertFiniteNumber(amount, 'amount')
|
|
883
873
|
const months = -amount * 3
|
|
884
874
|
assertFiniteNumber(months, 'amount')
|
|
885
|
-
return addDuration(date,
|
|
875
|
+
return addDuration(date, 'months', months, 'subQuarters')
|
|
886
876
|
}
|
|
887
877
|
|
|
888
878
|
// ─── getters / setters (date-fns / Date month & weekday indexing) ─
|
|
@@ -915,27 +905,51 @@ export function getDate(date) {
|
|
|
915
905
|
* @returns {number}
|
|
916
906
|
*/
|
|
917
907
|
export function getDay(date) {
|
|
918
|
-
return toPlainDate(date)
|
|
908
|
+
return PlainDateFns.dayOfWeek(toPlainDate(date))
|
|
919
909
|
}
|
|
920
910
|
|
|
921
911
|
/** @param {DayInput} date @returns {number} */
|
|
922
912
|
export function getDayOfYear(date) {
|
|
923
|
-
return toPlainDate(date)
|
|
913
|
+
return PlainDateFns.dayOfYear(toPlainDate(date))
|
|
924
914
|
}
|
|
925
915
|
|
|
926
916
|
/** @param {DayInput} date @returns {number} */
|
|
927
917
|
export function getDaysInMonth(date) {
|
|
928
|
-
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)
|
|
929
943
|
}
|
|
930
944
|
|
|
931
945
|
/** Quarter 1…4. @param {DayInput} date @returns {number} */
|
|
932
946
|
export function getQuarter(date) {
|
|
933
|
-
return
|
|
947
|
+
return quarterOf(toPlainDate(date))
|
|
934
948
|
}
|
|
935
949
|
|
|
936
950
|
/** @param {DayInput} date @returns {boolean} */
|
|
937
951
|
export function isLeapYear(date) {
|
|
938
|
-
return toPlainDate(date)
|
|
952
|
+
return PlainDateFns.inLeapYear(toPlainDate(date))
|
|
939
953
|
}
|
|
940
954
|
|
|
941
955
|
/**
|
|
@@ -946,7 +960,7 @@ export function isLeapYear(date) {
|
|
|
946
960
|
export function setYear(date, year) {
|
|
947
961
|
assertFiniteNumber(year, 'year')
|
|
948
962
|
const d = toPlainDate(date)
|
|
949
|
-
return guardRange('setYear', () => toDayString(
|
|
963
|
+
return guardRange('setYear', () => toDayString(PlainDateFns.withFields(d, { year })))
|
|
950
964
|
}
|
|
951
965
|
|
|
952
966
|
/**
|
|
@@ -960,7 +974,7 @@ export function setMonth(date, month) {
|
|
|
960
974
|
throw new RangeError('daymath: month must be 1…12 (1=January)')
|
|
961
975
|
}
|
|
962
976
|
const d = toPlainDate(date)
|
|
963
|
-
return guardRange('setMonth', () => toDayString(
|
|
977
|
+
return guardRange('setMonth', () => toDayString(PlainDateFns.withFields(d, { month })))
|
|
964
978
|
}
|
|
965
979
|
|
|
966
980
|
/**
|
|
@@ -972,7 +986,9 @@ export function setMonth(date, month) {
|
|
|
972
986
|
export function setDate(date, dayOfMonth) {
|
|
973
987
|
assertFiniteNumber(dayOfMonth, 'day')
|
|
974
988
|
const d = toPlainDate(date)
|
|
975
|
-
return guardRange('setDate', () =>
|
|
989
|
+
return guardRange('setDate', () =>
|
|
990
|
+
toDayString(PlainDateFns.withFields(d, { day: dayOfMonth })),
|
|
991
|
+
)
|
|
976
992
|
}
|
|
977
993
|
|
|
978
994
|
// ─── start / end of unit ───────────────────────────────────────────
|
|
@@ -984,41 +1000,53 @@ export function setDate(date, dayOfMonth) {
|
|
|
984
1000
|
/** @param {DayInput} date @returns {string} */
|
|
985
1001
|
export function startOfMonth(date) {
|
|
986
1002
|
const d = toPlainDate(date)
|
|
987
|
-
return guardRange('startOfMonth', () =>
|
|
1003
|
+
return guardRange('startOfMonth', () =>
|
|
1004
|
+
toDayString(PlainDateFns.withFields(d, { day: 1 })),
|
|
1005
|
+
)
|
|
988
1006
|
}
|
|
989
1007
|
|
|
990
1008
|
/** @param {DayInput} date @returns {string} */
|
|
991
1009
|
export function endOfMonth(date) {
|
|
992
1010
|
const d = toPlainDate(date)
|
|
993
|
-
return guardRange('endOfMonth', () =>
|
|
1011
|
+
return guardRange('endOfMonth', () =>
|
|
1012
|
+
toDayString(PlainDateFns.withFields(d, { day: PlainDateFns.daysInMonth(d) })),
|
|
1013
|
+
)
|
|
994
1014
|
}
|
|
995
1015
|
|
|
996
1016
|
/** @param {DayInput} date @returns {string} */
|
|
997
1017
|
export function startOfYear(date) {
|
|
998
1018
|
const d = toPlainDate(date)
|
|
999
|
-
return guardRange('startOfYear', () =>
|
|
1019
|
+
return guardRange('startOfYear', () =>
|
|
1020
|
+
toDayString(PlainDateFns.withFields(d, { month: 1, day: 1 })),
|
|
1021
|
+
)
|
|
1000
1022
|
}
|
|
1001
1023
|
|
|
1002
1024
|
/** @param {DayInput} date @returns {string} */
|
|
1003
1025
|
export function endOfYear(date) {
|
|
1004
1026
|
const d = toPlainDate(date)
|
|
1005
|
-
return guardRange('endOfYear', () =>
|
|
1027
|
+
return guardRange('endOfYear', () =>
|
|
1028
|
+
toDayString(PlainDateFns.withFields(d, { month: 12, day: 31 })),
|
|
1029
|
+
)
|
|
1006
1030
|
}
|
|
1007
1031
|
|
|
1008
1032
|
/** @param {DayInput} date @returns {string} */
|
|
1009
1033
|
export function startOfQuarter(date) {
|
|
1010
1034
|
const d = toPlainDate(date)
|
|
1011
|
-
const month = (
|
|
1012
|
-
return guardRange('startOfQuarter', () =>
|
|
1035
|
+
const month = (quarterOf(d) - 1) * 3 + 1
|
|
1036
|
+
return guardRange('startOfQuarter', () =>
|
|
1037
|
+
toDayString(PlainDateFns.withFields(d, { month, day: 1 })),
|
|
1038
|
+
)
|
|
1013
1039
|
}
|
|
1014
1040
|
|
|
1015
1041
|
/** @param {DayInput} date @returns {string} */
|
|
1016
1042
|
export function endOfQuarter(date) {
|
|
1017
1043
|
const d = toPlainDate(date)
|
|
1018
|
-
const month =
|
|
1044
|
+
const month = quarterOf(d) * 3
|
|
1019
1045
|
return guardRange('endOfQuarter', () => {
|
|
1020
|
-
const mid =
|
|
1021
|
-
return toDayString(
|
|
1046
|
+
const mid = PlainDateFns.withFields(d, { month, day: 1 })
|
|
1047
|
+
return toDayString(
|
|
1048
|
+
PlainDateFns.withFields(mid, { day: PlainDateFns.daysInMonth(mid) }),
|
|
1049
|
+
)
|
|
1022
1050
|
})
|
|
1023
1051
|
}
|
|
1024
1052
|
|
|
@@ -1030,19 +1058,19 @@ export function endOfQuarter(date) {
|
|
|
1030
1058
|
export function startOfWeek(date, options) {
|
|
1031
1059
|
const d = toPlainDate(date)
|
|
1032
1060
|
const diff = daysIntoWeek(d, options)
|
|
1033
|
-
return guardRange('startOfWeek', () => toDayString(
|
|
1061
|
+
return guardRange('startOfWeek', () => toDayString(PlainDateFns.subtractDays(d, diff)))
|
|
1034
1062
|
}
|
|
1035
1063
|
|
|
1036
1064
|
/**
|
|
1037
1065
|
* How far the day sits past the start of its week.
|
|
1038
|
-
* @param {
|
|
1066
|
+
* @param {PlainDateRecord} d
|
|
1039
1067
|
* @param {WeekOptions} [options]
|
|
1040
1068
|
* @returns {number}
|
|
1041
1069
|
*/
|
|
1042
1070
|
function daysIntoWeek(d, options) {
|
|
1043
1071
|
const weekStartsOn = weekStartsOnFrom(options)
|
|
1044
1072
|
// mod 7 makes weekStartsOn 0 and 7 identical, so both spellings of Sunday work
|
|
1045
|
-
return (
|
|
1073
|
+
return (PlainDateFns.dayOfWeek(d) - weekStartsOn + 7) % 7
|
|
1046
1074
|
}
|
|
1047
1075
|
|
|
1048
1076
|
/**
|
|
@@ -1055,7 +1083,7 @@ export function endOfWeek(date, options) {
|
|
|
1055
1083
|
const diff = daysIntoWeek(d, options)
|
|
1056
1084
|
// one guard for the whole walk, so a failure at either end says endOfWeek
|
|
1057
1085
|
return guardRange('endOfWeek', () =>
|
|
1058
|
-
toDayString(
|
|
1086
|
+
toDayString(PlainDateFns.addDays(PlainDateFns.subtractDays(d, diff), 6)),
|
|
1059
1087
|
)
|
|
1060
1088
|
}
|
|
1061
1089
|
|
|
@@ -1070,7 +1098,26 @@ export function endOfWeek(date, options) {
|
|
|
1070
1098
|
export function differenceInDays(dateLeft, dateRight) {
|
|
1071
1099
|
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
1072
1100
|
const right = toPlainDate(dateRight, 'dateRight')
|
|
1073
|
-
|
|
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
|
|
1074
1121
|
}
|
|
1075
1122
|
|
|
1076
1123
|
/**
|
|
@@ -1105,16 +1152,16 @@ export function differenceInMonths(dateLeft, dateRight) {
|
|
|
1105
1152
|
// 29-day gap, and two days 543 ISO years apart measured 0.
|
|
1106
1153
|
const left = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1107
1154
|
const right = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1108
|
-
const sign =
|
|
1155
|
+
const sign = PlainDateFns.compare(left, right)
|
|
1109
1156
|
if (sign === 0) return 0
|
|
1110
|
-
const diff = Math.abs(
|
|
1157
|
+
const diff = Math.abs(calendarMonthsBetween(left, right))
|
|
1111
1158
|
if (diff < 1) return 0
|
|
1112
1159
|
const [earlier, later] = sign > 0 ? [right, left] : [left, right]
|
|
1113
1160
|
// Where `earlier` lands after `diff` months: same year-month as `later` by
|
|
1114
1161
|
// construction, on `earlier`'s day clamped to that month's length. Compared
|
|
1115
1162
|
// as day numbers rather than built as a date, because the landing can sit
|
|
1116
1163
|
// past the maximum PlainDate even when both operands are inside the range.
|
|
1117
|
-
const landingDay = Math.min(earlier.day,
|
|
1164
|
+
const landingDay = Math.min(earlier.day, PlainDateFns.daysInMonth(later))
|
|
1118
1165
|
const isLastMonthNotFull = landingDay > later.day
|
|
1119
1166
|
return sign * (diff - +isLastMonthNotFull) || 0
|
|
1120
1167
|
}
|
|
@@ -1128,7 +1175,7 @@ export function differenceInMonths(dateLeft, dateRight) {
|
|
|
1128
1175
|
export function differenceInCalendarMonths(dateLeft, dateRight) {
|
|
1129
1176
|
const left = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1130
1177
|
const right = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1131
|
-
return (left
|
|
1178
|
+
return calendarMonthsBetween(left, right)
|
|
1132
1179
|
}
|
|
1133
1180
|
|
|
1134
1181
|
/**
|
|
@@ -1145,7 +1192,7 @@ export function differenceInYears(dateLeft, dateRight) {
|
|
|
1145
1192
|
// `isoOf` on both: a measurement, so it ignores the year LABEL. See differenceInMonths.
|
|
1146
1193
|
const left = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1147
1194
|
const right = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1148
|
-
const sign =
|
|
1195
|
+
const sign = PlainDateFns.compare(left, right)
|
|
1149
1196
|
if (sign === 0) return 0
|
|
1150
1197
|
const diff = Math.abs(left.year - right.year)
|
|
1151
1198
|
if (diff < 1) return 0
|
|
@@ -1156,7 +1203,9 @@ export function differenceInYears(dateLeft, dateRight) {
|
|
|
1156
1203
|
// Compared field by field rather than built as a date, because the landing
|
|
1157
1204
|
// can sit past the maximum PlainDate even with both operands inside the range.
|
|
1158
1205
|
const landingDay =
|
|
1159
|
-
earlier.month === 2 && earlier.day === 29 && !
|
|
1206
|
+
earlier.month === 2 && earlier.day === 29 && !PlainDateFns.inLeapYear(later)
|
|
1207
|
+
? 28
|
|
1208
|
+
: earlier.day
|
|
1160
1209
|
const isLastYearNotFull =
|
|
1161
1210
|
earlier.month > later.month ||
|
|
1162
1211
|
(earlier.month === later.month && landingDay > later.day)
|
|
@@ -1199,7 +1248,7 @@ export function differenceInQuarters(dateLeft, dateRight) {
|
|
|
1199
1248
|
export function differenceInCalendarQuarters(dateLeft, dateRight) {
|
|
1200
1249
|
const left = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1201
1250
|
const right = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1202
|
-
return (left.year - right.year) * 4 + (
|
|
1251
|
+
return (left.year - right.year) * 4 + (quarterOf(left) - quarterOf(right))
|
|
1203
1252
|
}
|
|
1204
1253
|
|
|
1205
1254
|
// ─── compare / equal ───────────────────────────────────────────────
|
|
@@ -1211,10 +1260,8 @@ export function differenceInCalendarQuarters(dateLeft, dateRight) {
|
|
|
1211
1260
|
*/
|
|
1212
1261
|
export function isBefore(date, dateToCompare) {
|
|
1213
1262
|
return (
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
toPlainDate(dateToCompare, 'dateToCompare'),
|
|
1217
|
-
) < 0
|
|
1263
|
+
PlainDateFns.compare(toPlainDate(date), toPlainDate(dateToCompare, 'dateToCompare')) <
|
|
1264
|
+
0
|
|
1218
1265
|
)
|
|
1219
1266
|
}
|
|
1220
1267
|
|
|
@@ -1225,10 +1272,8 @@ export function isBefore(date, dateToCompare) {
|
|
|
1225
1272
|
*/
|
|
1226
1273
|
export function isAfter(date, dateToCompare) {
|
|
1227
1274
|
return (
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
toPlainDate(dateToCompare, 'dateToCompare'),
|
|
1231
|
-
) > 0
|
|
1275
|
+
PlainDateFns.compare(toPlainDate(date), toPlainDate(dateToCompare, 'dateToCompare')) >
|
|
1276
|
+
0
|
|
1232
1277
|
)
|
|
1233
1278
|
}
|
|
1234
1279
|
|
|
@@ -1240,7 +1285,7 @@ export function isAfter(date, dateToCompare) {
|
|
|
1240
1285
|
*/
|
|
1241
1286
|
export function isEqual(dateLeft, dateRight) {
|
|
1242
1287
|
return (
|
|
1243
|
-
|
|
1288
|
+
PlainDateFns.compare(
|
|
1244
1289
|
toPlainDate(dateLeft, 'dateLeft'),
|
|
1245
1290
|
toPlainDate(dateRight, 'dateRight'),
|
|
1246
1291
|
) === 0
|
|
@@ -1265,9 +1310,9 @@ export function isSameWeek(dateLeft, dateRight, options) {
|
|
|
1265
1310
|
return guardRange(
|
|
1266
1311
|
'isSameWeek',
|
|
1267
1312
|
() =>
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1313
|
+
PlainDateFns.compare(
|
|
1314
|
+
PlainDateFns.subtractDays(left, daysIntoWeek(left, options)),
|
|
1315
|
+
PlainDateFns.subtractDays(right, daysIntoWeek(right, options)),
|
|
1271
1316
|
) === 0,
|
|
1272
1317
|
)
|
|
1273
1318
|
}
|
|
@@ -1305,7 +1350,7 @@ export function isSameYear(dateLeft, dateRight) {
|
|
|
1305
1350
|
export function isSameQuarter(dateLeft, dateRight) {
|
|
1306
1351
|
const a = isoOf(toPlainDate(dateLeft, 'dateLeft'))
|
|
1307
1352
|
const b = isoOf(toPlainDate(dateRight, 'dateRight'))
|
|
1308
|
-
return a.year === b.year &&
|
|
1353
|
+
return a.year === b.year && quarterOf(a) === quarterOf(b)
|
|
1309
1354
|
}
|
|
1310
1355
|
|
|
1311
1356
|
/**
|
|
@@ -1315,7 +1360,7 @@ export function isSameQuarter(dateLeft, dateRight) {
|
|
|
1315
1360
|
*/
|
|
1316
1361
|
export function compareAsc(dateLeft, dateRight) {
|
|
1317
1362
|
return /** @type {-1 | 0 | 1} */ (
|
|
1318
|
-
|
|
1363
|
+
PlainDateFns.compare(
|
|
1319
1364
|
toPlainDate(dateLeft, 'dateLeft'),
|
|
1320
1365
|
toPlainDate(dateRight, 'dateRight'),
|
|
1321
1366
|
)
|
|
@@ -1341,7 +1386,7 @@ export function min(dates) {
|
|
|
1341
1386
|
return toDayString(
|
|
1342
1387
|
dates
|
|
1343
1388
|
.map((d) => toPlainDate(d))
|
|
1344
|
-
.reduce((a, b) => (
|
|
1389
|
+
.reduce((a, b) => (PlainDateFns.compare(a, b) <= 0 ? a : b)),
|
|
1345
1390
|
)
|
|
1346
1391
|
}
|
|
1347
1392
|
|
|
@@ -1354,7 +1399,7 @@ export function max(dates) {
|
|
|
1354
1399
|
return toDayString(
|
|
1355
1400
|
dates
|
|
1356
1401
|
.map((d) => toPlainDate(d))
|
|
1357
|
-
.reduce((a, b) => (
|
|
1402
|
+
.reduce((a, b) => (PlainDateFns.compare(a, b) >= 0 ? a : b)),
|
|
1358
1403
|
)
|
|
1359
1404
|
}
|
|
1360
1405
|
|
|
@@ -1402,7 +1447,7 @@ export function isFirstDayOfMonth(date) {
|
|
|
1402
1447
|
/** @param {DayInput} date @returns {boolean} */
|
|
1403
1448
|
export function isLastDayOfMonth(date) {
|
|
1404
1449
|
const d = toPlainDate(date)
|
|
1405
|
-
return d.day ===
|
|
1450
|
+
return d.day === PlainDateFns.daysInMonth(d)
|
|
1406
1451
|
}
|
|
1407
1452
|
|
|
1408
1453
|
// ─── intervals ─────────────────────────────────────────────────────
|
|
@@ -1414,7 +1459,7 @@ export function isLastDayOfMonth(date) {
|
|
|
1414
1459
|
*/
|
|
1415
1460
|
export function eachDayOfInterval(interval) {
|
|
1416
1461
|
const { start, end } = toInterval(interval)
|
|
1417
|
-
if (
|
|
1462
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1418
1463
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1419
1464
|
}
|
|
1420
1465
|
/** @type {string[]} */
|
|
@@ -1424,8 +1469,8 @@ export function eachDayOfInterval(interval) {
|
|
|
1424
1469
|
// PlainDate (+275760-09-13) throws
|
|
1425
1470
|
for (;;) {
|
|
1426
1471
|
out.push(toDayString(cur))
|
|
1427
|
-
if (
|
|
1428
|
-
cur =
|
|
1472
|
+
if (PlainDateFns.compare(cur, end) >= 0) break
|
|
1473
|
+
cur = PlainDateFns.addDays(cur, 1)
|
|
1429
1474
|
}
|
|
1430
1475
|
return out
|
|
1431
1476
|
}
|
|
@@ -1437,22 +1482,22 @@ export function eachDayOfInterval(interval) {
|
|
|
1437
1482
|
*/
|
|
1438
1483
|
export function eachMonthOfInterval(interval) {
|
|
1439
1484
|
const { start, end } = toInterval(interval)
|
|
1440
|
-
if (
|
|
1485
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1441
1486
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1442
1487
|
}
|
|
1443
1488
|
/** @type {string[]} */
|
|
1444
1489
|
const out = []
|
|
1445
1490
|
// the 1st of start's month can sit below the minimum PlainDate
|
|
1446
1491
|
const [cur0, last] = guardRange('eachMonthOfInterval', () => [
|
|
1447
|
-
|
|
1448
|
-
|
|
1492
|
+
PlainDateFns.withFields(start, { day: 1 }),
|
|
1493
|
+
PlainDateFns.withFields(end, { day: 1 }),
|
|
1449
1494
|
])
|
|
1450
1495
|
let cur = cur0
|
|
1451
1496
|
// same boundary rule as eachDayOfInterval
|
|
1452
1497
|
for (;;) {
|
|
1453
1498
|
out.push(toDayString(cur))
|
|
1454
|
-
if (
|
|
1455
|
-
cur =
|
|
1499
|
+
if (PlainDateFns.compare(cur, last) >= 0) break
|
|
1500
|
+
cur = PlainDateFns.addMonths(cur, 1)
|
|
1456
1501
|
}
|
|
1457
1502
|
return out
|
|
1458
1503
|
}
|
|
@@ -1464,7 +1509,7 @@ export function eachMonthOfInterval(interval) {
|
|
|
1464
1509
|
*/
|
|
1465
1510
|
export function eachYearOfInterval(interval) {
|
|
1466
1511
|
const { start, end } = toInterval(interval)
|
|
1467
|
-
if (
|
|
1512
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1468
1513
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1469
1514
|
}
|
|
1470
1515
|
/** @type {string[]} */
|
|
@@ -1476,11 +1521,11 @@ export function eachYearOfInterval(interval) {
|
|
|
1476
1521
|
// Compare ISO years, then add. Testing the loop condition AFTER the push is what keeps the
|
|
1477
1522
|
// top edge working: Jan 1 of +275760 is valid, and adding a year to it is not.
|
|
1478
1523
|
const lastIsoYear = isoOf(end).year
|
|
1479
|
-
let cur =
|
|
1524
|
+
let cur = PlainDateFns.withFields(start, { month: 1, day: 1 })
|
|
1480
1525
|
for (;;) {
|
|
1481
1526
|
out.push(toDayString(cur))
|
|
1482
1527
|
if (isoOf(cur).year >= lastIsoYear) break
|
|
1483
|
-
cur =
|
|
1528
|
+
cur = PlainDateFns.addYears(cur, 1)
|
|
1484
1529
|
}
|
|
1485
1530
|
})
|
|
1486
1531
|
return out
|
|
@@ -1495,12 +1540,10 @@ export function eachYearOfInterval(interval) {
|
|
|
1495
1540
|
export function isWithinInterval(date, interval) {
|
|
1496
1541
|
const d = toPlainDate(date)
|
|
1497
1542
|
const { start, end } = toInterval(interval)
|
|
1498
|
-
if (
|
|
1543
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1499
1544
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1500
1545
|
}
|
|
1501
|
-
return (
|
|
1502
|
-
Temporal.PlainDate.compare(d, start) >= 0 && Temporal.PlainDate.compare(d, end) <= 0
|
|
1503
|
-
)
|
|
1546
|
+
return PlainDateFns.compare(d, start) >= 0 && PlainDateFns.compare(d, end) <= 0
|
|
1504
1547
|
}
|
|
1505
1548
|
|
|
1506
1549
|
/**
|
|
@@ -1512,11 +1555,11 @@ export function isWithinInterval(date, interval) {
|
|
|
1512
1555
|
export function clamp(date, interval) {
|
|
1513
1556
|
const d = toPlainDate(date)
|
|
1514
1557
|
const { start, end } = toInterval(interval)
|
|
1515
|
-
if (
|
|
1558
|
+
if (PlainDateFns.compare(start, end) > 0) {
|
|
1516
1559
|
throw new RangeError('daymath: interval start must not be after end')
|
|
1517
1560
|
}
|
|
1518
|
-
if (
|
|
1519
|
-
if (
|
|
1561
|
+
if (PlainDateFns.compare(d, start) < 0) return toDayString(start)
|
|
1562
|
+
if (PlainDateFns.compare(d, end) > 0) return toDayString(end)
|
|
1520
1563
|
return toDayString(d)
|
|
1521
1564
|
}
|
|
1522
1565
|
|
|
@@ -1530,22 +1573,21 @@ export function clamp(date, interval) {
|
|
|
1530
1573
|
export function areIntervalsOverlapping(intervalLeft, intervalRight, options) {
|
|
1531
1574
|
const a = toInterval(intervalLeft)
|
|
1532
1575
|
const b = toInterval(intervalRight)
|
|
1533
|
-
if (
|
|
1576
|
+
if (PlainDateFns.compare(a.start, a.end) > 0) {
|
|
1534
1577
|
throw new RangeError('daymath: intervalLeft start must not be after end')
|
|
1535
1578
|
}
|
|
1536
|
-
if (
|
|
1579
|
+
if (PlainDateFns.compare(b.start, b.end) > 0) {
|
|
1537
1580
|
throw new RangeError('daymath: intervalRight start must not be after end')
|
|
1538
1581
|
}
|
|
1539
1582
|
const inclusive = options?.inclusive ?? false
|
|
1540
1583
|
if (inclusive) {
|
|
1541
1584
|
return (
|
|
1542
|
-
|
|
1543
|
-
|
|
1585
|
+
PlainDateFns.compare(a.start, b.end) <= 0 &&
|
|
1586
|
+
PlainDateFns.compare(b.start, a.end) <= 0
|
|
1544
1587
|
)
|
|
1545
1588
|
}
|
|
1546
1589
|
// date-fns default: touch-at-endpoint is NOT overlap
|
|
1547
1590
|
return (
|
|
1548
|
-
|
|
1549
|
-
Temporal.PlainDate.compare(b.start, a.end) < 0
|
|
1591
|
+
PlainDateFns.compare(a.start, b.end) < 0 && PlainDateFns.compare(b.start, a.end) < 0
|
|
1550
1592
|
)
|
|
1551
1593
|
}
|