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.
Files changed (3) hide show
  1. package/README.md +10 -5
  2. package/index.js +227 -185
  3. 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
- Uses global `Temporal` when present; otherwise [`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill),
159
- which does that resolution itself so on a runtime with native Temporal the polyfill steps aside.
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 needs `temporal-polyfill/full`, because the base build cannot construct
316
- them where the runtime has no native Temporal. That costs **4.2 kB gzip**, and it is what makes
317
- the answers identical on every runtime rather than only on the ones with native Temporal.
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
- // The selection is HERE on purpose, and it must not be removed again.
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
- // `temporal-polyfill`'s base entry resolves `globalThis.Temporal || bundled` itself, so importing
5
- // it gave native Temporal for free. But that base build can construct only iso8601 and gregory, so
6
- // it cannot serve the calendar rule below. `temporal-polyfill/full` can construct sixteen — and its
7
- // entry is a bare re-export with NO selection, so importing it alone silenced native Temporal on
8
- // every runtime, including Node 26 and Deno. Round 1 of a review caught that.
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
- // Round 2 then caught the naive repair. A global `Temporal` is NOT necessarily native: an app doing
11
- // `import 'temporal-polyfill/global'` installs the BASE build, and the polyfill's own README calls
12
- // that the most common entry point. Selecting it blindly cost daymath three of its four calendars
13
- // on Node 20 and 22, which have no native Temporal, and the error blamed the caller's calendar
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
- // The probe names no calendar. It asks the runtime for one, which keeps the "method, not a list"
18
- // rule that governs the calendar surface below.
19
- //
20
- // Measured: the selection costs 17 B gzip, because the polyfill ships either way — a bundler cannot
21
- // know at build time whether the runtime has Temporal, which is why FUTURE.md records dynamic
22
- // import as a dead end. Native and the bundled full build agree on every calendar daymath touches:
23
- // 464 date/calendar pairs over the four accepted calendars from 1900 to 2100, plus the eleven
24
- // refused ones, with zero disagreements.
25
- //
26
- // Native matters for three reasons beyond taste. daymath's contract is Temporal's behaviour, and
27
- // native IS Temporal. The deno CI lane exists to prove the polyfill and the standard agree, and it
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
- // `Temporal` is a const now, not an imported namespace, so it cannot carry types. `PlainDate` has
173
- // seven use sites, so a typedef earns itself. `Instant` and `ZonedDateTime` have one each and are
174
- // inlined, because measured the typedef plus its use is longer than the inline form.
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 = Temporal.PlainDate.from(probe)
286
- const dated = iso.withCalendar(calendar)
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 {PlainDate}
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
- const plain = Temporal.PlainDate.from(bare)
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 ? plain : plain.withCalendar(calendar)
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 {PlainDate} plain
409
- * @returns {PlainDate}
371
+ * @param {PlainDateRecord} plain
372
+ * @returns {PlainDateRecord}
410
373
  */
411
374
  function isoOf(plain) {
412
- return plain.calendarId === 'iso8601' ? plain : plain.withCalendar('iso8601')
375
+ return plain.calendarId === 'iso8601'
376
+ ? plain
377
+ : PlainDateFns.withCalendar(plain, ISO_CALENDAR)
413
378
  }
414
379
 
415
- /** @param {PlainDate} plain @returns {string} */
380
+ /** @param {PlainDateRecord} plain @returns {string} */
416
381
  function toDayString(plain) {
417
- return plain.toString()
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 {{ days?: number, months?: number, years?: number }} delta
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, delta, label) {
437
+ function addDuration(date, unit, amount, label) {
464
438
  const d = toPlainDate(date)
465
- return guardRange(label, () => toDayString(d.add(delta)))
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: PlainDate, end: PlainDate }}
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').Temporal.Instant | undefined} */
563
+ /** @type {import('temporal-polyfill/fns/Instant').Record | undefined} */
584
564
  let instant
585
- /** @type {import('temporal-polyfill').Temporal.ZonedDateTime | undefined} */
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 = Temporal.ZonedDateTime.from(moment)
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 = Temporal.Instant.from(moment)
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.from` accepts exactly what `Temporal.Now` accepts and reads
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
- Temporal.ZonedDateTime.from({ timeZone: zone, year: 1970, month: 1, day: 1 })
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
- const plain = guardRange('day', () => zoned.toPlainDate())
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
- instant.toZonedDateTimeISO(zone).toPlainDate().toString(),
694
+ PlainDateFns.toString(
695
+ ZonedFns.toPlainDate(InstantFns.toZonedDateTimeISO(instant, zone)),
696
+ ),
708
697
  )
709
698
  }
710
699
 
711
- if (!isMoment) return Temporal.Now.plainDateISO(zone).toString()
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
- Temporal.Instant.fromEpochMilliseconds(epochMs)
724
- .toZonedDateTimeISO(zone)
725
- .toPlainDate()
726
- .toString(),
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, { days: amount }, 'addDays')
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, { days: -amount }, 'subDays')
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, { days }, 'addWeeks')
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, { days }, 'subWeeks')
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, { months: amount }, 'addMonths')
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, { months: -amount }, 'subMonths')
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, { years: amount }, 'addYears')
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, { years: -amount }, 'subYears')
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, { months }, 'addQuarters')
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, { months }, 'subQuarters')
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).dayOfWeek
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).dayOfYear
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).daysInMonth
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 Math.ceil(toPlainDate(date).month / 3)
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).inLeapYear
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(d.with({ year })))
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(d.with({ month })))
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', () => toDayString(d.with({ day: dayOfMonth })))
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', () => toDayString(d.with({ day: 1 })))
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', () => toDayString(d.with({ day: d.daysInMonth })))
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', () => toDayString(d.with({ month: 1, day: 1 })))
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', () => toDayString(d.with({ month: 12, day: 31 })))
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 = (getQuarter(d) - 1) * 3 + 1
1012
- return guardRange('startOfQuarter', () => toDayString(d.with({ month, day: 1 })))
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 = getQuarter(d) * 3
1044
+ const month = quarterOf(d) * 3
1019
1045
  return guardRange('endOfQuarter', () => {
1020
- const mid = d.with({ month, day: 1 })
1021
- return toDayString(mid.with({ day: mid.daysInMonth }))
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(d.subtract({ days: diff })))
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 {PlainDate} d
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 (d.dayOfWeek - weekStartsOn + 7) % 7
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(d.subtract({ days: diff }).add({ days: 6 })),
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
- return isoOf(left).since(isoOf(right), { largestUnit: 'day' }).days
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 = Temporal.PlainDate.compare(left, right)
1155
+ const sign = PlainDateFns.compare(left, right)
1109
1156
  if (sign === 0) return 0
1110
- const diff = Math.abs(differenceInCalendarMonths(left, right))
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, later.daysInMonth)
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.year - right.year) * 12 + (left.month - right.month)
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 = Temporal.PlainDate.compare(left, right)
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 && !later.inLeapYear ? 28 : earlier.day
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 + (getQuarter(left) - getQuarter(right))
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
- Temporal.PlainDate.compare(
1215
- toPlainDate(date),
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
- Temporal.PlainDate.compare(
1229
- toPlainDate(date),
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
- Temporal.PlainDate.compare(
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
- Temporal.PlainDate.compare(
1269
- left.subtract({ days: daysIntoWeek(left, options) }),
1270
- right.subtract({ days: daysIntoWeek(right, options) }),
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 && getQuarter(a) === getQuarter(b)
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
- Temporal.PlainDate.compare(
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) => (Temporal.PlainDate.compare(a, b) <= 0 ? 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) => (Temporal.PlainDate.compare(a, b) >= 0 ? 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 === d.daysInMonth
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 (Temporal.PlainDate.compare(start, end) > 0) {
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 (Temporal.PlainDate.compare(cur, end) >= 0) break
1428
- cur = cur.add({ days: 1 })
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 (Temporal.PlainDate.compare(start, end) > 0) {
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
- start.with({ day: 1 }),
1448
- end.with({ day: 1 }),
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 (Temporal.PlainDate.compare(cur, last) >= 0) break
1455
- cur = cur.add({ months: 1 })
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 (Temporal.PlainDate.compare(start, end) > 0) {
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 = start.with({ month: 1, day: 1 })
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 = cur.add({ years: 1 })
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 (Temporal.PlainDate.compare(start, end) > 0) {
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 (Temporal.PlainDate.compare(start, end) > 0) {
1558
+ if (PlainDateFns.compare(start, end) > 0) {
1516
1559
  throw new RangeError('daymath: interval start must not be after end')
1517
1560
  }
1518
- if (Temporal.PlainDate.compare(d, start) < 0) return toDayString(start)
1519
- if (Temporal.PlainDate.compare(d, end) > 0) return toDayString(end)
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 (Temporal.PlainDate.compare(a.start, a.end) > 0) {
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 (Temporal.PlainDate.compare(b.start, b.end) > 0) {
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
- Temporal.PlainDate.compare(a.start, b.end) <= 0 &&
1543
- Temporal.PlainDate.compare(b.start, a.end) <= 0
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
- Temporal.PlainDate.compare(a.start, b.end) < 0 &&
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daymath",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Calendar date math (ISO 8601 day / PlainDate). date-fns-shaped. No Date. No time zones.",
5
5
  "type": "module",
6
6
  "main": "./index.js",