daymath 0.1.0 → 0.2.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 +54 -22
- package/index.d.ts +90 -2
- package/index.js +536 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# daymath
|
|
2
2
|
|
|
3
|
-
Calendar date math for `YYYY-MM-DD` strings. **date-fns-shaped** names. **Temporal.PlainDate** under the hood.
|
|
3
|
+
Calendar date math for **ISO 8601** `YYYY-MM-DD` strings. **date-fns-shaped** names. **Temporal.PlainDate** under the hood.
|
|
4
4
|
|
|
5
5
|
No `Date`. No time zones. No silent “local now.”
|
|
6
6
|
|
|
@@ -20,34 +20,66 @@ import {
|
|
|
20
20
|
addMonths,
|
|
21
21
|
differenceInDays,
|
|
22
22
|
isBefore,
|
|
23
|
-
|
|
23
|
+
isSameDay,
|
|
24
|
+
startOfMonth,
|
|
25
|
+
eachDayOfInterval,
|
|
24
26
|
} from 'daymath'
|
|
25
27
|
|
|
26
|
-
addDays('2026-08-06', 1)
|
|
27
|
-
addMonths('2026-01-31', 1)
|
|
28
|
+
addDays('2026-08-06', 1) // '2026-08-07'
|
|
29
|
+
addMonths('2026-01-31', 1) // '2026-02-28' (constrain)
|
|
28
30
|
differenceInDays('2026-08-06', '2026-08-01') // 5
|
|
29
|
-
isBefore('2026-08-05', '2026-08-06')
|
|
30
|
-
|
|
31
|
+
isBefore('2026-08-05', '2026-08-06') // true
|
|
32
|
+
isSameDay('2026-08-06', '2026-08-06') // true (alias of isEqual)
|
|
33
|
+
startOfMonth('2026-08-06') // '2026-08-01'
|
|
34
|
+
eachDayOfInterval({
|
|
35
|
+
start: '2026-08-05',
|
|
36
|
+
end: '2026-08-07',
|
|
37
|
+
}) // ['2026-08-05', '2026-08-06', '2026-08-07']
|
|
31
38
|
```
|
|
32
39
|
|
|
33
|
-
Inputs
|
|
34
|
-
Outputs
|
|
40
|
+
**Inputs:** ISO 8601 `YYYY-MM-DD` or `Temporal.PlainDate`.
|
|
41
|
+
**Outputs:** always `YYYY-MM-DD` string (for math helpers).
|
|
35
42
|
|
|
36
|
-
`Date` throws. Time-bearing
|
|
43
|
+
`Date` throws. Time-bearing strings throw. Sloppy forms like `2026-8-6` throw.
|
|
37
44
|
|
|
38
|
-
##
|
|
45
|
+
## date-fns parity notes
|
|
39
46
|
|
|
40
|
-
|
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
| Topic | daymath |
|
|
48
|
+
|-------|---------|
|
|
49
|
+
| Value type | `YYYY-MM-DD` string (not `Date`) |
|
|
50
|
+
| `isSameDay` | Alias of `isEqual` (same calendar day) |
|
|
51
|
+
| `getMonth` / `setMonth` | **0–11** like Date/date-fns (0 = January) |
|
|
52
|
+
| `getDay` | **0–6** like Date/date-fns (0 = Sunday) |
|
|
53
|
+
| `weekStartsOn` | `0` = Sunday … `6` = Saturday (default `0`) |
|
|
54
|
+
| Intervals | `{ start, end }` inclusive for `each*` / `isWithin` / `clamp` |
|
|
55
|
+
| `areIntervalsOverlapping` | Default `{ inclusive: false }` (date-fns); pass `true` for closed |
|
|
56
|
+
|
|
57
|
+
## API (0.2)
|
|
58
|
+
|
|
59
|
+
### Parse / format
|
|
60
|
+
`parse` · `format` · `isValid`
|
|
61
|
+
|
|
62
|
+
### Add / sub
|
|
63
|
+
`addDays` / `subDays` · `addWeeks` / `subWeeks` · `addMonths` / `subMonths` · `addYears` / `subYears` · `addQuarters` / `subQuarters`
|
|
64
|
+
|
|
65
|
+
### Get / set
|
|
66
|
+
`getYear` · `getMonth` · `getDate` · `getDay` · `getDayOfYear` · `getDaysInMonth` · `getQuarter` · `isLeapYear`
|
|
67
|
+
`setYear` · `setMonth` · `setDate`
|
|
68
|
+
|
|
69
|
+
### Start / end
|
|
70
|
+
`startOfMonth` / `endOfMonth` · `startOfYear` / `endOfYear` · `startOfQuarter` / `endOfQuarter` · `startOfWeek` / `endOfWeek`
|
|
71
|
+
|
|
72
|
+
### Differences
|
|
73
|
+
`differenceInDays` · `differenceInWeeks` · `differenceInMonths` · `differenceInCalendarMonths` · `differenceInYears` · `differenceInCalendarYears` · `differenceInQuarters` · `differenceInCalendarQuarters`
|
|
74
|
+
|
|
75
|
+
### Compare
|
|
76
|
+
`isBefore` · `isAfter` · `isEqual` · `isSameDay` · `isSameWeek` · `isSameMonth` · `isSameYear` · `isSameQuarter` · `compareAsc` · `compareDesc` · `min` · `max`
|
|
77
|
+
|
|
78
|
+
### Weekday / month edges
|
|
79
|
+
`isSunday`…`isSaturday` · `isWeekend` · `isFirstDayOfMonth` · `isLastDayOfMonth`
|
|
80
|
+
|
|
81
|
+
### Intervals
|
|
82
|
+
`eachDayOfInterval` · `eachMonthOfInterval` · `eachYearOfInterval` · `isWithinInterval` · `clamp` · `areIntervalsOverlapping`
|
|
51
83
|
|
|
52
84
|
Amounts must be finite integers.
|
|
53
85
|
|
|
@@ -57,7 +89,7 @@ Uses global `Temporal` when present; otherwise [`temporal-polyfill`](https://www
|
|
|
57
89
|
|
|
58
90
|
## Types
|
|
59
91
|
|
|
60
|
-
Ships `index.d.ts` (no TypeScript compile step
|
|
92
|
+
Ships `index.d.ts` (no TypeScript compile step). Source is plain JS.
|
|
61
93
|
|
|
62
94
|
## License
|
|
63
95
|
|
package/index.d.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import type { Temporal } from 'temporal-polyfill'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Calendar day input: `YYYY-MM-DD` string, or a Temporal.PlainDate.
|
|
4
|
+
* Calendar day input: ISO 8601 `YYYY-MM-DD` string, or a Temporal.PlainDate.
|
|
5
5
|
* `Date` is rejected at runtime.
|
|
6
6
|
*/
|
|
7
7
|
export type DayInput = string | Temporal.PlainDate
|
|
8
8
|
|
|
9
|
+
/** Inclusive calendar-day interval (date-fns shape). */
|
|
10
|
+
export type Interval = {
|
|
11
|
+
start: DayInput
|
|
12
|
+
end: DayInput
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Week options. `weekStartsOn`: 0 = Sunday … 6 = Saturday (date-fns default 0).
|
|
17
|
+
*/
|
|
18
|
+
export type WeekOptions = {
|
|
19
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export function isValid(value: unknown): boolean
|
|
10
23
|
|
|
11
|
-
/** Validate / normalize to `YYYY-MM-DD`. */
|
|
24
|
+
/** Validate / normalize to ISO 8601 `YYYY-MM-DD`. */
|
|
12
25
|
export function parse(date: DayInput): string
|
|
13
26
|
|
|
14
27
|
/** Format as `YYYY-MM-DD` (only pattern supported). */
|
|
@@ -22,16 +35,91 @@ export function addMonths(date: DayInput, amount: number): string
|
|
|
22
35
|
export function subMonths(date: DayInput, amount: number): string
|
|
23
36
|
export function addYears(date: DayInput, amount: number): string
|
|
24
37
|
export function subYears(date: DayInput, amount: number): string
|
|
38
|
+
export function addQuarters(date: DayInput, amount: number): string
|
|
39
|
+
export function subQuarters(date: DayInput, amount: number): string
|
|
40
|
+
|
|
41
|
+
/** Full year number. */
|
|
42
|
+
export function getYear(date: DayInput): number
|
|
43
|
+
/** Month index like Date/date-fns: 0 = January … 11 = December. */
|
|
44
|
+
export function getMonth(date: DayInput): number
|
|
45
|
+
/** Day of month 1…31. */
|
|
46
|
+
export function getDate(date: DayInput): number
|
|
47
|
+
/** Weekday like Date/date-fns: 0 = Sunday … 6 = Saturday. */
|
|
48
|
+
export function getDay(date: DayInput): number
|
|
49
|
+
export function getDayOfYear(date: DayInput): number
|
|
50
|
+
export function getDaysInMonth(date: DayInput): number
|
|
51
|
+
/** Quarter 1…4. */
|
|
52
|
+
export function getQuarter(date: DayInput): number
|
|
53
|
+
export function isLeapYear(date: DayInput): boolean
|
|
54
|
+
|
|
55
|
+
export function setYear(date: DayInput, year: number): string
|
|
56
|
+
/** `month`: 0 = January … 11 = December (date-fns). */
|
|
57
|
+
export function setMonth(date: DayInput, month: number): string
|
|
58
|
+
export function setDate(date: DayInput, dayOfMonth: number): string
|
|
59
|
+
|
|
60
|
+
export function startOfMonth(date: DayInput): string
|
|
61
|
+
export function endOfMonth(date: DayInput): string
|
|
62
|
+
export function startOfYear(date: DayInput): string
|
|
63
|
+
export function endOfYear(date: DayInput): string
|
|
64
|
+
export function startOfQuarter(date: DayInput): string
|
|
65
|
+
export function endOfQuarter(date: DayInput): string
|
|
66
|
+
export function startOfWeek(date: DayInput, options?: WeekOptions): string
|
|
67
|
+
export function endOfWeek(date: DayInput, options?: WeekOptions): string
|
|
25
68
|
|
|
26
69
|
/** Full days: `dateLeft − dateRight` (date-fns argument order). */
|
|
27
70
|
export function differenceInDays(dateLeft: DayInput, dateRight: DayInput): number
|
|
71
|
+
export function differenceInWeeks(dateLeft: DayInput, dateRight: DayInput): number
|
|
72
|
+
export function differenceInMonths(dateLeft: DayInput, dateRight: DayInput): number
|
|
73
|
+
export function differenceInCalendarMonths(dateLeft: DayInput, dateRight: DayInput): number
|
|
74
|
+
export function differenceInYears(dateLeft: DayInput, dateRight: DayInput): number
|
|
75
|
+
export function differenceInCalendarYears(dateLeft: DayInput, dateRight: DayInput): number
|
|
76
|
+
export function differenceInQuarters(dateLeft: DayInput, dateRight: DayInput): number
|
|
77
|
+
export function differenceInCalendarQuarters(dateLeft: DayInput, dateRight: DayInput): number
|
|
28
78
|
|
|
29
79
|
export function isBefore(date: DayInput, dateToCompare: DayInput): boolean
|
|
30
80
|
export function isAfter(date: DayInput, dateToCompare: DayInput): boolean
|
|
81
|
+
/** Same calendar day. */
|
|
31
82
|
export function isEqual(dateLeft: DayInput, dateRight: DayInput): boolean
|
|
83
|
+
/** Alias of `isEqual` (date-fns name). */
|
|
84
|
+
export const isSameDay: typeof isEqual
|
|
85
|
+
|
|
86
|
+
export function isSameWeek(
|
|
87
|
+
dateLeft: DayInput,
|
|
88
|
+
dateRight: DayInput,
|
|
89
|
+
options?: WeekOptions,
|
|
90
|
+
): boolean
|
|
91
|
+
export function isSameMonth(dateLeft: DayInput, dateRight: DayInput): boolean
|
|
92
|
+
export function isSameYear(dateLeft: DayInput, dateRight: DayInput): boolean
|
|
93
|
+
export function isSameQuarter(dateLeft: DayInput, dateRight: DayInput): boolean
|
|
32
94
|
|
|
33
95
|
export function compareAsc(dateLeft: DayInput, dateRight: DayInput): -1 | 0 | 1
|
|
34
96
|
export function compareDesc(dateLeft: DayInput, dateRight: DayInput): -1 | 0 | 1
|
|
35
97
|
|
|
36
98
|
export function min(dates: DayInput[]): string
|
|
37
99
|
export function max(dates: DayInput[]): string
|
|
100
|
+
|
|
101
|
+
export function isSunday(date: DayInput): boolean
|
|
102
|
+
export function isMonday(date: DayInput): boolean
|
|
103
|
+
export function isTuesday(date: DayInput): boolean
|
|
104
|
+
export function isWednesday(date: DayInput): boolean
|
|
105
|
+
export function isThursday(date: DayInput): boolean
|
|
106
|
+
export function isFriday(date: DayInput): boolean
|
|
107
|
+
export function isSaturday(date: DayInput): boolean
|
|
108
|
+
export function isWeekend(date: DayInput): boolean
|
|
109
|
+
export function isFirstDayOfMonth(date: DayInput): boolean
|
|
110
|
+
export function isLastDayOfMonth(date: DayInput): boolean
|
|
111
|
+
|
|
112
|
+
export function eachDayOfInterval(interval: Interval): string[]
|
|
113
|
+
export function eachMonthOfInterval(interval: Interval): string[]
|
|
114
|
+
export function eachYearOfInterval(interval: Interval): string[]
|
|
115
|
+
export function isWithinInterval(date: DayInput, interval: Interval): boolean
|
|
116
|
+
export function clamp(date: DayInput, interval: Interval): string
|
|
117
|
+
/**
|
|
118
|
+
* date-fns default: `inclusive: false` (touching endpoints only is not overlap).
|
|
119
|
+
* Pass `{ inclusive: true }` for closed intervals.
|
|
120
|
+
*/
|
|
121
|
+
export function areIntervalsOverlapping(
|
|
122
|
+
intervalLeft: Interval,
|
|
123
|
+
intervalRight: Interval,
|
|
124
|
+
options?: { inclusive?: boolean },
|
|
125
|
+
): boolean
|
package/index.js
CHANGED
|
@@ -4,6 +4,17 @@ import { Temporal as TemporalPolyfill } from 'temporal-polyfill'
|
|
|
4
4
|
const Temporal = globalThis.Temporal ?? TemporalPolyfill
|
|
5
5
|
|
|
6
6
|
/** @typedef {string | Temporal.PlainDate} DayInput */
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} Interval
|
|
9
|
+
* @property {DayInput} start
|
|
10
|
+
* @property {DayInput} end
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} WeekOptions
|
|
14
|
+
* @property {0|1|2|3|4|5|6} [weekStartsOn] 0=Sun … 6=Sat (date-fns default 0)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ─── core conversion ───────────────────────────────────────────────
|
|
7
18
|
|
|
8
19
|
/**
|
|
9
20
|
* Reject Date and non-calendar values. Accept YYYY-MM-DD string or PlainDate.
|
|
@@ -18,7 +29,7 @@ function toPlainDate(value, label = 'date') {
|
|
|
18
29
|
)
|
|
19
30
|
}
|
|
20
31
|
if (typeof value === 'string') {
|
|
21
|
-
// Strict calendar
|
|
32
|
+
// Strict ISO 8601 calendar date: YYYY-MM-DD only.
|
|
22
33
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
23
34
|
throw new RangeError(
|
|
24
35
|
`daymath: ${label} must be YYYY-MM-DD (got ${JSON.stringify(value)})`,
|
|
@@ -45,6 +56,55 @@ function toDayString(plain) {
|
|
|
45
56
|
return plain.toString()
|
|
46
57
|
}
|
|
47
58
|
|
|
59
|
+
/** Temporal ISO weekday 1=Mon…7=Sun → JS/date-fns 0=Sun…6=Sat */
|
|
60
|
+
function isoToJsWeekday(isoDayOfWeek) {
|
|
61
|
+
return isoDayOfWeek === 7 ? 0 : isoDayOfWeek
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {unknown} n @param {string} label */
|
|
65
|
+
function assertFiniteNumber(n, label) {
|
|
66
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) {
|
|
67
|
+
throw new TypeError(`daymath: ${label} must be a finite number`)
|
|
68
|
+
}
|
|
69
|
+
if (!Number.isInteger(n)) {
|
|
70
|
+
throw new RangeError(`daymath: ${label} must be an integer`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @param {unknown} dates */
|
|
75
|
+
function assertNonEmptyDates(dates) {
|
|
76
|
+
if (!Array.isArray(dates) || dates.length === 0) {
|
|
77
|
+
throw new RangeError('daymath: expected a non-empty array of dates')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {WeekOptions} [options]
|
|
83
|
+
* @returns {0|1|2|3|4|5|6}
|
|
84
|
+
*/
|
|
85
|
+
function weekStartsOnFrom(options) {
|
|
86
|
+
const w = options?.weekStartsOn ?? 0
|
|
87
|
+
if (!Number.isInteger(w) || w < 0 || w > 6) {
|
|
88
|
+
throw new RangeError('daymath: weekStartsOn must be an integer 0…6 (0=Sun)')
|
|
89
|
+
}
|
|
90
|
+
return /** @type {0|1|2|3|4|5|6} */ (w)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {unknown} interval
|
|
95
|
+
* @returns {{ start: Temporal.PlainDate, end: Temporal.PlainDate }}
|
|
96
|
+
*/
|
|
97
|
+
function toInterval(interval) {
|
|
98
|
+
if (interval == null || typeof interval !== 'object') {
|
|
99
|
+
throw new TypeError('daymath: interval must be { start, end }')
|
|
100
|
+
}
|
|
101
|
+
const start = toPlainDate(/** @type {Interval} */ (interval).start, 'start')
|
|
102
|
+
const end = toPlainDate(/** @type {Interval} */ (interval).end, 'end')
|
|
103
|
+
return { start, end }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── parse / format / valid ────────────────────────────────────────
|
|
107
|
+
|
|
48
108
|
/** @param {unknown} value */
|
|
49
109
|
export function isValid(value) {
|
|
50
110
|
try {
|
|
@@ -56,7 +116,7 @@ export function isValid(value) {
|
|
|
56
116
|
}
|
|
57
117
|
|
|
58
118
|
/**
|
|
59
|
-
* Validate / normalize a calendar day string.
|
|
119
|
+
* Validate / normalize a calendar day string (ISO 8601 YYYY-MM-DD).
|
|
60
120
|
* @param {DayInput} date
|
|
61
121
|
* @returns {string}
|
|
62
122
|
*/
|
|
@@ -65,7 +125,7 @@ export function parse(date) {
|
|
|
65
125
|
}
|
|
66
126
|
|
|
67
127
|
/**
|
|
68
|
-
* Format as YYYY-MM-DD (only supported pattern
|
|
128
|
+
* Format as YYYY-MM-DD (only supported pattern).
|
|
69
129
|
* @param {DayInput} date
|
|
70
130
|
* @param {string} [pattern='yyyy-MM-dd']
|
|
71
131
|
* @returns {string}
|
|
@@ -79,6 +139,8 @@ export function format(date, pattern = 'yyyy-MM-dd') {
|
|
|
79
139
|
return toDayString(toPlainDate(date))
|
|
80
140
|
}
|
|
81
141
|
|
|
142
|
+
// ─── add / sub ─────────────────────────────────────────────────────
|
|
143
|
+
|
|
82
144
|
/**
|
|
83
145
|
* @param {DayInput} date
|
|
84
146
|
* @param {number} amount
|
|
@@ -120,7 +182,7 @@ export function subWeeks(date, amount) {
|
|
|
120
182
|
}
|
|
121
183
|
|
|
122
184
|
/**
|
|
123
|
-
* Calendar months (
|
|
185
|
+
* Calendar months (overflow constrain — Jan 31 + 1 month → Feb 28/29).
|
|
124
186
|
* @param {DayInput} date
|
|
125
187
|
* @param {number} amount
|
|
126
188
|
* @returns {string}
|
|
@@ -160,6 +222,174 @@ export function subYears(date, amount) {
|
|
|
160
222
|
return addYears(date, -amount)
|
|
161
223
|
}
|
|
162
224
|
|
|
225
|
+
/**
|
|
226
|
+
* @param {DayInput} date
|
|
227
|
+
* @param {number} amount
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
export function addQuarters(date, amount) {
|
|
231
|
+
assertFiniteNumber(amount, 'amount')
|
|
232
|
+
return addMonths(date, amount * 3)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {DayInput} date
|
|
237
|
+
* @param {number} amount
|
|
238
|
+
* @returns {string}
|
|
239
|
+
*/
|
|
240
|
+
export function subQuarters(date, amount) {
|
|
241
|
+
assertFiniteNumber(amount, 'amount')
|
|
242
|
+
return addQuarters(date, -amount)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── getters / setters (date-fns / Date month & weekday indexing) ─
|
|
246
|
+
|
|
247
|
+
/** @param {DayInput} date @returns {number} */
|
|
248
|
+
export function getYear(date) {
|
|
249
|
+
return toPlainDate(date).year
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Month index like Date/date-fns: 0 = January … 11 = December.
|
|
254
|
+
* @param {DayInput} date
|
|
255
|
+
* @returns {number}
|
|
256
|
+
*/
|
|
257
|
+
export function getMonth(date) {
|
|
258
|
+
return toPlainDate(date).month - 1
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Day of month 1…31. @param {DayInput} date @returns {number} */
|
|
262
|
+
export function getDate(date) {
|
|
263
|
+
return toPlainDate(date).day
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Weekday like Date/date-fns: 0 = Sunday … 6 = Saturday.
|
|
268
|
+
* @param {DayInput} date
|
|
269
|
+
* @returns {number}
|
|
270
|
+
*/
|
|
271
|
+
export function getDay(date) {
|
|
272
|
+
return isoToJsWeekday(toPlainDate(date).dayOfWeek)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @param {DayInput} date @returns {number} */
|
|
276
|
+
export function getDayOfYear(date) {
|
|
277
|
+
return toPlainDate(date).dayOfYear
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** @param {DayInput} date @returns {number} */
|
|
281
|
+
export function getDaysInMonth(date) {
|
|
282
|
+
return toPlainDate(date).daysInMonth
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Quarter 1…4. @param {DayInput} date @returns {number} */
|
|
286
|
+
export function getQuarter(date) {
|
|
287
|
+
return Math.ceil(toPlainDate(date).month / 3)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
291
|
+
export function isLeapYear(date) {
|
|
292
|
+
return toPlainDate(date).inLeapYear
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @param {DayInput} date
|
|
297
|
+
* @param {number} year
|
|
298
|
+
* @returns {string}
|
|
299
|
+
*/
|
|
300
|
+
export function setYear(date, year) {
|
|
301
|
+
assertFiniteNumber(year, 'year')
|
|
302
|
+
return toDayString(toPlainDate(date).with({ year }))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {DayInput} date
|
|
307
|
+
* @param {number} month 0 = January … 11 = December (date-fns)
|
|
308
|
+
* @returns {string}
|
|
309
|
+
*/
|
|
310
|
+
export function setMonth(date, month) {
|
|
311
|
+
assertFiniteNumber(month, 'month')
|
|
312
|
+
if (month < 0 || month > 11) {
|
|
313
|
+
throw new RangeError('daymath: month must be 0…11 (0=January)')
|
|
314
|
+
}
|
|
315
|
+
return toDayString(toPlainDate(date).with({ month: month + 1 }))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @param {DayInput} date
|
|
320
|
+
* @param {number} dayOfMonth
|
|
321
|
+
* @returns {string}
|
|
322
|
+
*/
|
|
323
|
+
export function setDate(date, dayOfMonth) {
|
|
324
|
+
assertFiniteNumber(dayOfMonth, 'day')
|
|
325
|
+
return toDayString(toPlainDate(date).with({ day: dayOfMonth }))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─── start / end of unit ───────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
/** @param {DayInput} date @returns {string} */
|
|
331
|
+
export function startOfMonth(date) {
|
|
332
|
+
const d = toPlainDate(date)
|
|
333
|
+
return toDayString(d.with({ day: 1 }))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** @param {DayInput} date @returns {string} */
|
|
337
|
+
export function endOfMonth(date) {
|
|
338
|
+
const d = toPlainDate(date)
|
|
339
|
+
return toDayString(d.with({ day: d.daysInMonth }))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** @param {DayInput} date @returns {string} */
|
|
343
|
+
export function startOfYear(date) {
|
|
344
|
+
const d = toPlainDate(date)
|
|
345
|
+
return toDayString(d.with({ month: 1, day: 1 }))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** @param {DayInput} date @returns {string} */
|
|
349
|
+
export function endOfYear(date) {
|
|
350
|
+
const d = toPlainDate(date)
|
|
351
|
+
return toDayString(d.with({ month: 12, day: 31 }))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** @param {DayInput} date @returns {string} */
|
|
355
|
+
export function startOfQuarter(date) {
|
|
356
|
+
const d = toPlainDate(date)
|
|
357
|
+
const month = (getQuarter(d) - 1) * 3 + 1
|
|
358
|
+
return toDayString(d.with({ month, day: 1 }))
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** @param {DayInput} date @returns {string} */
|
|
362
|
+
export function endOfQuarter(date) {
|
|
363
|
+
const d = toPlainDate(date)
|
|
364
|
+
const month = getQuarter(d) * 3
|
|
365
|
+
const mid = d.with({ month, day: 1 })
|
|
366
|
+
return toDayString(mid.with({ day: mid.daysInMonth }))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* @param {DayInput} date
|
|
371
|
+
* @param {WeekOptions} [options]
|
|
372
|
+
* @returns {string}
|
|
373
|
+
*/
|
|
374
|
+
export function startOfWeek(date, options) {
|
|
375
|
+
const d = toPlainDate(date)
|
|
376
|
+
const weekStartsOn = weekStartsOnFrom(options)
|
|
377
|
+
const day = isoToJsWeekday(d.dayOfWeek)
|
|
378
|
+
const diff = (day - weekStartsOn + 7) % 7
|
|
379
|
+
return toDayString(d.subtract({ days: diff }))
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @param {DayInput} date
|
|
384
|
+
* @param {WeekOptions} [options]
|
|
385
|
+
* @returns {string}
|
|
386
|
+
*/
|
|
387
|
+
export function endOfWeek(date, options) {
|
|
388
|
+
return addDays(startOfWeek(date, options), 6)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── differences ───────────────────────────────────────────────────
|
|
392
|
+
|
|
163
393
|
/**
|
|
164
394
|
* Full calendar days: dateLeft − dateRight (date-fns order).
|
|
165
395
|
* @param {DayInput} dateLeft
|
|
@@ -172,6 +402,89 @@ export function differenceInDays(dateLeft, dateRight) {
|
|
|
172
402
|
return left.since(right, { largestUnit: 'day' }).days
|
|
173
403
|
}
|
|
174
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Full weeks (trunc toward 0), like date-fns.
|
|
407
|
+
* @param {DayInput} dateLeft
|
|
408
|
+
* @param {DayInput} dateRight
|
|
409
|
+
* @returns {number}
|
|
410
|
+
*/
|
|
411
|
+
export function differenceInWeeks(dateLeft, dateRight) {
|
|
412
|
+
return Math.trunc(differenceInDays(dateLeft, dateRight) / 7)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Full months (signed), Temporal since with largestUnit month.
|
|
417
|
+
* @param {DayInput} dateLeft
|
|
418
|
+
* @param {DayInput} dateRight
|
|
419
|
+
* @returns {number}
|
|
420
|
+
*/
|
|
421
|
+
export function differenceInMonths(dateLeft, dateRight) {
|
|
422
|
+
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
423
|
+
const right = toPlainDate(dateRight, 'dateRight')
|
|
424
|
+
const dur = left.since(right, { largestUnit: 'month' })
|
|
425
|
+
return dur.months
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Calendar month index diff: (yL−yR)*12 + (mL−mR). Ignores day-of-month.
|
|
430
|
+
* @param {DayInput} dateLeft
|
|
431
|
+
* @param {DayInput} dateRight
|
|
432
|
+
* @returns {number}
|
|
433
|
+
*/
|
|
434
|
+
export function differenceInCalendarMonths(dateLeft, dateRight) {
|
|
435
|
+
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
436
|
+
const right = toPlainDate(dateRight, 'dateRight')
|
|
437
|
+
return (left.year - right.year) * 12 + (left.month - right.month)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Full years (signed).
|
|
442
|
+
* @param {DayInput} dateLeft
|
|
443
|
+
* @param {DayInput} dateRight
|
|
444
|
+
* @returns {number}
|
|
445
|
+
*/
|
|
446
|
+
export function differenceInYears(dateLeft, dateRight) {
|
|
447
|
+
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
448
|
+
const right = toPlainDate(dateRight, 'dateRight')
|
|
449
|
+
return left.since(right, { largestUnit: 'year' }).years
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Calendar year number diff. Ignores month/day.
|
|
454
|
+
* @param {DayInput} dateLeft
|
|
455
|
+
* @param {DayInput} dateRight
|
|
456
|
+
* @returns {number}
|
|
457
|
+
*/
|
|
458
|
+
export function differenceInCalendarYears(dateLeft, dateRight) {
|
|
459
|
+
return getYear(dateLeft) - getYear(dateRight)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Full quarters (trunc toward 0 of calendar-month/3 style via months).
|
|
464
|
+
* @param {DayInput} dateLeft
|
|
465
|
+
* @param {DayInput} dateRight
|
|
466
|
+
* @returns {number}
|
|
467
|
+
*/
|
|
468
|
+
export function differenceInQuarters(dateLeft, dateRight) {
|
|
469
|
+
return Math.trunc(differenceInMonths(dateLeft, dateRight) / 3)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Calendar quarter index diff.
|
|
474
|
+
* @param {DayInput} dateLeft
|
|
475
|
+
* @param {DayInput} dateRight
|
|
476
|
+
* @returns {number}
|
|
477
|
+
*/
|
|
478
|
+
export function differenceInCalendarQuarters(dateLeft, dateRight) {
|
|
479
|
+
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
480
|
+
const right = toPlainDate(dateRight, 'dateRight')
|
|
481
|
+
return (
|
|
482
|
+
(left.year - right.year) * 4 + (getQuarter(left) - getQuarter(right))
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ─── compare / equal ───────────────────────────────────────────────
|
|
487
|
+
|
|
175
488
|
/**
|
|
176
489
|
* @param {DayInput} date
|
|
177
490
|
* @param {DayInput} dateToCompare
|
|
@@ -201,6 +514,7 @@ export function isAfter(date, dateToCompare) {
|
|
|
201
514
|
}
|
|
202
515
|
|
|
203
516
|
/**
|
|
517
|
+
* Same calendar day.
|
|
204
518
|
* @param {DayInput} dateLeft
|
|
205
519
|
* @param {DayInput} dateRight
|
|
206
520
|
* @returns {boolean}
|
|
@@ -214,6 +528,50 @@ export function isEqual(dateLeft, dateRight) {
|
|
|
214
528
|
)
|
|
215
529
|
}
|
|
216
530
|
|
|
531
|
+
/** Alias of `isEqual` (date-fns name for same calendar day). */
|
|
532
|
+
export const isSameDay = isEqual
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* @param {DayInput} dateLeft
|
|
536
|
+
* @param {DayInput} dateRight
|
|
537
|
+
* @param {WeekOptions} [options]
|
|
538
|
+
* @returns {boolean}
|
|
539
|
+
*/
|
|
540
|
+
export function isSameWeek(dateLeft, dateRight, options) {
|
|
541
|
+
return isEqual(startOfWeek(dateLeft, options), startOfWeek(dateRight, options))
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* @param {DayInput} dateLeft
|
|
546
|
+
* @param {DayInput} dateRight
|
|
547
|
+
* @returns {boolean}
|
|
548
|
+
*/
|
|
549
|
+
export function isSameMonth(dateLeft, dateRight) {
|
|
550
|
+
const a = toPlainDate(dateLeft, 'dateLeft')
|
|
551
|
+
const b = toPlainDate(dateRight, 'dateRight')
|
|
552
|
+
return a.year === b.year && a.month === b.month
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* @param {DayInput} dateLeft
|
|
557
|
+
* @param {DayInput} dateRight
|
|
558
|
+
* @returns {boolean}
|
|
559
|
+
*/
|
|
560
|
+
export function isSameYear(dateLeft, dateRight) {
|
|
561
|
+
return getYear(dateLeft) === getYear(dateRight)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* @param {DayInput} dateLeft
|
|
566
|
+
* @param {DayInput} dateRight
|
|
567
|
+
* @returns {boolean}
|
|
568
|
+
*/
|
|
569
|
+
export function isSameQuarter(dateLeft, dateRight) {
|
|
570
|
+
const a = toPlainDate(dateLeft, 'dateLeft')
|
|
571
|
+
const b = toPlainDate(dateRight, 'dateRight')
|
|
572
|
+
return a.year === b.year && getQuarter(a) === getQuarter(b)
|
|
573
|
+
}
|
|
574
|
+
|
|
217
575
|
/**
|
|
218
576
|
* @param {DayInput} dateLeft
|
|
219
577
|
* @param {DayInput} dateRight
|
|
@@ -244,7 +602,9 @@ export function compareDesc(dateLeft, dateRight) {
|
|
|
244
602
|
export function min(dates) {
|
|
245
603
|
assertNonEmptyDates(dates)
|
|
246
604
|
return toDayString(
|
|
247
|
-
dates
|
|
605
|
+
dates
|
|
606
|
+
.map((d) => toPlainDate(d))
|
|
607
|
+
.reduce((a, b) => (Temporal.PlainDate.compare(a, b) <= 0 ? a : b)),
|
|
248
608
|
)
|
|
249
609
|
}
|
|
250
610
|
|
|
@@ -255,23 +615,183 @@ export function min(dates) {
|
|
|
255
615
|
export function max(dates) {
|
|
256
616
|
assertNonEmptyDates(dates)
|
|
257
617
|
return toDayString(
|
|
258
|
-
dates
|
|
618
|
+
dates
|
|
619
|
+
.map((d) => toPlainDate(d))
|
|
620
|
+
.reduce((a, b) => (Temporal.PlainDate.compare(a, b) >= 0 ? a : b)),
|
|
259
621
|
)
|
|
260
622
|
}
|
|
261
623
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
624
|
+
// ─── weekday predicates ────────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
627
|
+
export function isSunday(date) {
|
|
628
|
+
return getDay(date) === 0
|
|
629
|
+
}
|
|
630
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
631
|
+
export function isMonday(date) {
|
|
632
|
+
return getDay(date) === 1
|
|
633
|
+
}
|
|
634
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
635
|
+
export function isTuesday(date) {
|
|
636
|
+
return getDay(date) === 2
|
|
637
|
+
}
|
|
638
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
639
|
+
export function isWednesday(date) {
|
|
640
|
+
return getDay(date) === 3
|
|
641
|
+
}
|
|
642
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
643
|
+
export function isThursday(date) {
|
|
644
|
+
return getDay(date) === 4
|
|
645
|
+
}
|
|
646
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
647
|
+
export function isFriday(date) {
|
|
648
|
+
return getDay(date) === 5
|
|
649
|
+
}
|
|
650
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
651
|
+
export function isSaturday(date) {
|
|
652
|
+
return getDay(date) === 6
|
|
653
|
+
}
|
|
654
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
655
|
+
export function isWeekend(date) {
|
|
656
|
+
const d = getDay(date)
|
|
657
|
+
return d === 0 || d === 6
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
661
|
+
export function isFirstDayOfMonth(date) {
|
|
662
|
+
return getDate(date) === 1
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** @param {DayInput} date @returns {boolean} */
|
|
666
|
+
export function isLastDayOfMonth(date) {
|
|
667
|
+
const d = toPlainDate(date)
|
|
668
|
+
return d.day === d.daysInMonth
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ─── intervals ─────────────────────────────────────────────────────
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Inclusive day range. Throws if start > end.
|
|
675
|
+
* @param {Interval} interval
|
|
676
|
+
* @returns {string[]}
|
|
677
|
+
*/
|
|
678
|
+
export function eachDayOfInterval(interval) {
|
|
679
|
+
const { start, end } = toInterval(interval)
|
|
680
|
+
if (Temporal.PlainDate.compare(start, end) > 0) {
|
|
681
|
+
throw new RangeError('daymath: interval start must not be after end')
|
|
266
682
|
}
|
|
267
|
-
|
|
268
|
-
|
|
683
|
+
/** @type {string[]} */
|
|
684
|
+
const out = []
|
|
685
|
+
let cur = start
|
|
686
|
+
while (Temporal.PlainDate.compare(cur, end) <= 0) {
|
|
687
|
+
out.push(toDayString(cur))
|
|
688
|
+
cur = cur.add({ days: 1 })
|
|
269
689
|
}
|
|
690
|
+
return out
|
|
270
691
|
}
|
|
271
692
|
|
|
272
|
-
/**
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
693
|
+
/**
|
|
694
|
+
* First day of each month from start’s month through end’s month (date-fns shape).
|
|
695
|
+
* @param {Interval} interval
|
|
696
|
+
* @returns {string[]}
|
|
697
|
+
*/
|
|
698
|
+
export function eachMonthOfInterval(interval) {
|
|
699
|
+
const { start, end } = toInterval(interval)
|
|
700
|
+
if (Temporal.PlainDate.compare(start, end) > 0) {
|
|
701
|
+
throw new RangeError('daymath: interval start must not be after end')
|
|
276
702
|
}
|
|
703
|
+
/** @type {string[]} */
|
|
704
|
+
const out = []
|
|
705
|
+
let cur = start.with({ day: 1 })
|
|
706
|
+
const last = end.with({ day: 1 })
|
|
707
|
+
while (Temporal.PlainDate.compare(cur, last) <= 0) {
|
|
708
|
+
out.push(toDayString(cur))
|
|
709
|
+
cur = cur.add({ months: 1 })
|
|
710
|
+
}
|
|
711
|
+
return out
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Jan 1 of each year from start’s year through end’s year.
|
|
716
|
+
* @param {Interval} interval
|
|
717
|
+
* @returns {string[]}
|
|
718
|
+
*/
|
|
719
|
+
export function eachYearOfInterval(interval) {
|
|
720
|
+
const { start, end } = toInterval(interval)
|
|
721
|
+
if (Temporal.PlainDate.compare(start, end) > 0) {
|
|
722
|
+
throw new RangeError('daymath: interval start must not be after end')
|
|
723
|
+
}
|
|
724
|
+
/** @type {string[]} */
|
|
725
|
+
const out = []
|
|
726
|
+
let y = start.year
|
|
727
|
+
while (y <= end.year) {
|
|
728
|
+
out.push(toDayString(Temporal.PlainDate.from({ year: y, month: 1, day: 1 })))
|
|
729
|
+
y += 1
|
|
730
|
+
}
|
|
731
|
+
return out
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Inclusive: start ≤ date ≤ end.
|
|
736
|
+
* @param {DayInput} date
|
|
737
|
+
* @param {Interval} interval
|
|
738
|
+
* @returns {boolean}
|
|
739
|
+
*/
|
|
740
|
+
export function isWithinInterval(date, interval) {
|
|
741
|
+
const d = toPlainDate(date)
|
|
742
|
+
const { start, end } = toInterval(interval)
|
|
743
|
+
if (Temporal.PlainDate.compare(start, end) > 0) {
|
|
744
|
+
throw new RangeError('daymath: interval start must not be after end')
|
|
745
|
+
}
|
|
746
|
+
return (
|
|
747
|
+
Temporal.PlainDate.compare(d, start) >= 0 &&
|
|
748
|
+
Temporal.PlainDate.compare(d, end) <= 0
|
|
749
|
+
)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Clamp date into [start, end].
|
|
754
|
+
* @param {DayInput} date
|
|
755
|
+
* @param {Interval} interval
|
|
756
|
+
* @returns {string}
|
|
757
|
+
*/
|
|
758
|
+
export function clamp(date, interval) {
|
|
759
|
+
const d = toPlainDate(date)
|
|
760
|
+
const { start, end } = toInterval(interval)
|
|
761
|
+
if (Temporal.PlainDate.compare(start, end) > 0) {
|
|
762
|
+
throw new RangeError('daymath: interval start must not be after end')
|
|
763
|
+
}
|
|
764
|
+
if (Temporal.PlainDate.compare(d, start) < 0) return toDayString(start)
|
|
765
|
+
if (Temporal.PlainDate.compare(d, end) > 0) return toDayString(end)
|
|
766
|
+
return toDayString(d)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Whether two inclusive intervals overlap.
|
|
771
|
+
* @param {Interval} intervalLeft
|
|
772
|
+
* @param {Interval} intervalRight
|
|
773
|
+
* @param {{ inclusive?: boolean }} [options] default inclusive true (date-fns default false uses half-open; we default true for plain days)
|
|
774
|
+
* @returns {boolean}
|
|
775
|
+
*/
|
|
776
|
+
export function areIntervalsOverlapping(intervalLeft, intervalRight, options) {
|
|
777
|
+
const a = toInterval(intervalLeft)
|
|
778
|
+
const b = toInterval(intervalRight)
|
|
779
|
+
if (Temporal.PlainDate.compare(a.start, a.end) > 0) {
|
|
780
|
+
throw new RangeError('daymath: intervalLeft start must not be after end')
|
|
781
|
+
}
|
|
782
|
+
if (Temporal.PlainDate.compare(b.start, b.end) > 0) {
|
|
783
|
+
throw new RangeError('daymath: intervalRight start must not be after end')
|
|
784
|
+
}
|
|
785
|
+
const inclusive = options?.inclusive ?? false
|
|
786
|
+
if (inclusive) {
|
|
787
|
+
return (
|
|
788
|
+
Temporal.PlainDate.compare(a.start, b.end) <= 0 &&
|
|
789
|
+
Temporal.PlainDate.compare(b.start, a.end) <= 0
|
|
790
|
+
)
|
|
791
|
+
}
|
|
792
|
+
// date-fns default: touch-at-endpoint is NOT overlap
|
|
793
|
+
return (
|
|
794
|
+
Temporal.PlainDate.compare(a.start, b.end) < 0 &&
|
|
795
|
+
Temporal.PlainDate.compare(b.start, a.end) < 0
|
|
796
|
+
)
|
|
277
797
|
}
|