daymath 0.0.1 → 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 +83 -6
- package/index.d.ts +125 -0
- package/index.js +796 -3
- package/package.json +20 -4
package/README.md
CHANGED
|
@@ -1,18 +1,95 @@
|
|
|
1
1
|
# daymath
|
|
2
2
|
|
|
3
|
-
Calendar date math
|
|
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
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install daymath
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Why
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
`Date` is a timestamp. Calendar work (“add one month”, “days between hire and start”) is not. This package only does plain calendar days.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import {
|
|
19
|
+
addDays,
|
|
20
|
+
addMonths,
|
|
21
|
+
differenceInDays,
|
|
22
|
+
isBefore,
|
|
23
|
+
isSameDay,
|
|
24
|
+
startOfMonth,
|
|
25
|
+
eachDayOfInterval,
|
|
26
|
+
} from 'daymath'
|
|
27
|
+
|
|
28
|
+
addDays('2026-08-06', 1) // '2026-08-07'
|
|
29
|
+
addMonths('2026-01-31', 1) // '2026-02-28' (constrain)
|
|
30
|
+
differenceInDays('2026-08-06', '2026-08-01') // 5
|
|
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']
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Inputs:** ISO 8601 `YYYY-MM-DD` or `Temporal.PlainDate`.
|
|
41
|
+
**Outputs:** always `YYYY-MM-DD` string (for math helpers).
|
|
42
|
+
|
|
43
|
+
`Date` throws. Time-bearing strings throw. Sloppy forms like `2026-8-6` throw.
|
|
44
|
+
|
|
45
|
+
## date-fns parity notes
|
|
46
|
+
|
|
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`
|
|
83
|
+
|
|
84
|
+
Amounts must be finite integers.
|
|
85
|
+
|
|
86
|
+
## Temporal
|
|
87
|
+
|
|
88
|
+
Uses global `Temporal` when present; otherwise [`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill).
|
|
89
|
+
|
|
90
|
+
## Types
|
|
91
|
+
|
|
92
|
+
Ships `index.d.ts` (no TypeScript compile step). Source is plain JS.
|
|
16
93
|
|
|
17
94
|
## License
|
|
18
95
|
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Temporal } from 'temporal-polyfill'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calendar day input: ISO 8601 `YYYY-MM-DD` string, or a Temporal.PlainDate.
|
|
5
|
+
* `Date` is rejected at runtime.
|
|
6
|
+
*/
|
|
7
|
+
export type DayInput = string | Temporal.PlainDate
|
|
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
|
+
|
|
22
|
+
export function isValid(value: unknown): boolean
|
|
23
|
+
|
|
24
|
+
/** Validate / normalize to ISO 8601 `YYYY-MM-DD`. */
|
|
25
|
+
export function parse(date: DayInput): string
|
|
26
|
+
|
|
27
|
+
/** Format as `YYYY-MM-DD` (only pattern supported). */
|
|
28
|
+
export function format(date: DayInput, pattern?: 'yyyy-MM-dd' | 'YYYY-MM-DD'): string
|
|
29
|
+
|
|
30
|
+
export function addDays(date: DayInput, amount: number): string
|
|
31
|
+
export function subDays(date: DayInput, amount: number): string
|
|
32
|
+
export function addWeeks(date: DayInput, amount: number): string
|
|
33
|
+
export function subWeeks(date: DayInput, amount: number): string
|
|
34
|
+
export function addMonths(date: DayInput, amount: number): string
|
|
35
|
+
export function subMonths(date: DayInput, amount: number): string
|
|
36
|
+
export function addYears(date: DayInput, amount: number): string
|
|
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
|
|
68
|
+
|
|
69
|
+
/** Full days: `dateLeft − dateRight` (date-fns argument order). */
|
|
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
|
|
78
|
+
|
|
79
|
+
export function isBefore(date: DayInput, dateToCompare: DayInput): boolean
|
|
80
|
+
export function isAfter(date: DayInput, dateToCompare: DayInput): boolean
|
|
81
|
+
/** Same calendar day. */
|
|
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
|
|
94
|
+
|
|
95
|
+
export function compareAsc(dateLeft: DayInput, dateRight: DayInput): -1 | 0 | 1
|
|
96
|
+
export function compareDesc(dateLeft: DayInput, dateRight: DayInput): -1 | 0 | 1
|
|
97
|
+
|
|
98
|
+
export function min(dates: DayInput[]): string
|
|
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
|
@@ -1,4 +1,797 @@
|
|
|
1
|
-
/** daymath —
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/** daymath — calendar date math (YYYY-MM-DD). date-fns-shaped. No Date / time zones. */
|
|
2
|
+
import { Temporal as TemporalPolyfill } from 'temporal-polyfill'
|
|
3
|
+
|
|
4
|
+
const Temporal = globalThis.Temporal ?? TemporalPolyfill
|
|
5
|
+
|
|
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 ───────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reject Date and non-calendar values. Accept YYYY-MM-DD string or PlainDate.
|
|
21
|
+
* @param {unknown} value
|
|
22
|
+
* @param {string} label
|
|
23
|
+
* @returns {Temporal.PlainDate}
|
|
24
|
+
*/
|
|
25
|
+
function toPlainDate(value, label = 'date') {
|
|
26
|
+
if (value instanceof Date) {
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
`daymath: Date is not allowed for ${label} (pass YYYY-MM-DD string)`,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'string') {
|
|
32
|
+
// Strict ISO 8601 calendar date: YYYY-MM-DD only.
|
|
33
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
34
|
+
throw new RangeError(
|
|
35
|
+
`daymath: ${label} must be YYYY-MM-DD (got ${JSON.stringify(value)})`,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return Temporal.PlainDate.from(value)
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new RangeError(`daymath: invalid ${label} ${JSON.stringify(value)}`, {
|
|
42
|
+
cause: err,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (value instanceof Temporal.PlainDate) {
|
|
47
|
+
return value
|
|
48
|
+
}
|
|
49
|
+
throw new TypeError(
|
|
50
|
+
`daymath: ${label} must be YYYY-MM-DD string or Temporal.PlainDate`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {Temporal.PlainDate} plain @returns {string} */
|
|
55
|
+
function toDayString(plain) {
|
|
56
|
+
return plain.toString()
|
|
57
|
+
}
|
|
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
|
+
|
|
108
|
+
/** @param {unknown} value */
|
|
109
|
+
export function isValid(value) {
|
|
110
|
+
try {
|
|
111
|
+
toPlainDate(value)
|
|
112
|
+
return true
|
|
113
|
+
} catch {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate / normalize a calendar day string (ISO 8601 YYYY-MM-DD).
|
|
120
|
+
* @param {DayInput} date
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
export function parse(date) {
|
|
124
|
+
return toDayString(toPlainDate(date))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Format as YYYY-MM-DD (only supported pattern).
|
|
129
|
+
* @param {DayInput} date
|
|
130
|
+
* @param {string} [pattern='yyyy-MM-dd']
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
export function format(date, pattern = 'yyyy-MM-dd') {
|
|
134
|
+
if (pattern !== 'yyyy-MM-dd' && pattern !== 'YYYY-MM-DD') {
|
|
135
|
+
throw new RangeError(
|
|
136
|
+
`daymath: only "yyyy-MM-dd" format is supported (got ${JSON.stringify(pattern)})`,
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
return toDayString(toPlainDate(date))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── add / sub ─────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {DayInput} date
|
|
146
|
+
* @param {number} amount
|
|
147
|
+
* @returns {string}
|
|
148
|
+
*/
|
|
149
|
+
export function addDays(date, amount) {
|
|
150
|
+
assertFiniteNumber(amount, 'amount')
|
|
151
|
+
return toDayString(toPlainDate(date).add({ days: amount }))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {DayInput} date
|
|
156
|
+
* @param {number} amount
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
export function subDays(date, amount) {
|
|
160
|
+
assertFiniteNumber(amount, 'amount')
|
|
161
|
+
return addDays(date, -amount)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {DayInput} date
|
|
166
|
+
* @param {number} amount
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function addWeeks(date, amount) {
|
|
170
|
+
assertFiniteNumber(amount, 'amount')
|
|
171
|
+
return addDays(date, amount * 7)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {DayInput} date
|
|
176
|
+
* @param {number} amount
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
export function subWeeks(date, amount) {
|
|
180
|
+
assertFiniteNumber(amount, 'amount')
|
|
181
|
+
return addWeeks(date, -amount)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Calendar months (overflow constrain — Jan 31 + 1 month → Feb 28/29).
|
|
186
|
+
* @param {DayInput} date
|
|
187
|
+
* @param {number} amount
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
190
|
+
export function addMonths(date, amount) {
|
|
191
|
+
assertFiniteNumber(amount, 'amount')
|
|
192
|
+
return toDayString(toPlainDate(date).add({ months: amount }))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {DayInput} date
|
|
197
|
+
* @param {number} amount
|
|
198
|
+
* @returns {string}
|
|
199
|
+
*/
|
|
200
|
+
export function subMonths(date, amount) {
|
|
201
|
+
assertFiniteNumber(amount, 'amount')
|
|
202
|
+
return addMonths(date, -amount)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {DayInput} date
|
|
207
|
+
* @param {number} amount
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
export function addYears(date, amount) {
|
|
211
|
+
assertFiniteNumber(amount, 'amount')
|
|
212
|
+
return toDayString(toPlainDate(date).add({ years: amount }))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {DayInput} date
|
|
217
|
+
* @param {number} amount
|
|
218
|
+
* @returns {string}
|
|
219
|
+
*/
|
|
220
|
+
export function subYears(date, amount) {
|
|
221
|
+
assertFiniteNumber(amount, 'amount')
|
|
222
|
+
return addYears(date, -amount)
|
|
223
|
+
}
|
|
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
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Full calendar days: dateLeft − dateRight (date-fns order).
|
|
395
|
+
* @param {DayInput} dateLeft
|
|
396
|
+
* @param {DayInput} dateRight
|
|
397
|
+
* @returns {number}
|
|
398
|
+
*/
|
|
399
|
+
export function differenceInDays(dateLeft, dateRight) {
|
|
400
|
+
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
401
|
+
const right = toPlainDate(dateRight, 'dateRight')
|
|
402
|
+
return left.since(right, { largestUnit: 'day' }).days
|
|
403
|
+
}
|
|
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
|
+
|
|
488
|
+
/**
|
|
489
|
+
* @param {DayInput} date
|
|
490
|
+
* @param {DayInput} dateToCompare
|
|
491
|
+
* @returns {boolean}
|
|
492
|
+
*/
|
|
493
|
+
export function isBefore(date, dateToCompare) {
|
|
494
|
+
return (
|
|
495
|
+
Temporal.PlainDate.compare(
|
|
496
|
+
toPlainDate(date),
|
|
497
|
+
toPlainDate(dateToCompare, 'dateToCompare'),
|
|
498
|
+
) < 0
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* @param {DayInput} date
|
|
504
|
+
* @param {DayInput} dateToCompare
|
|
505
|
+
* @returns {boolean}
|
|
506
|
+
*/
|
|
507
|
+
export function isAfter(date, dateToCompare) {
|
|
508
|
+
return (
|
|
509
|
+
Temporal.PlainDate.compare(
|
|
510
|
+
toPlainDate(date),
|
|
511
|
+
toPlainDate(dateToCompare, 'dateToCompare'),
|
|
512
|
+
) > 0
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Same calendar day.
|
|
518
|
+
* @param {DayInput} dateLeft
|
|
519
|
+
* @param {DayInput} dateRight
|
|
520
|
+
* @returns {boolean}
|
|
521
|
+
*/
|
|
522
|
+
export function isEqual(dateLeft, dateRight) {
|
|
523
|
+
return (
|
|
524
|
+
Temporal.PlainDate.compare(
|
|
525
|
+
toPlainDate(dateLeft, 'dateLeft'),
|
|
526
|
+
toPlainDate(dateRight, 'dateRight'),
|
|
527
|
+
) === 0
|
|
528
|
+
)
|
|
529
|
+
}
|
|
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
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @param {DayInput} dateLeft
|
|
577
|
+
* @param {DayInput} dateRight
|
|
578
|
+
* @returns {-1 | 0 | 1}
|
|
579
|
+
*/
|
|
580
|
+
export function compareAsc(dateLeft, dateRight) {
|
|
581
|
+
return /** @type {-1 | 0 | 1} */ (
|
|
582
|
+
Temporal.PlainDate.compare(
|
|
583
|
+
toPlainDate(dateLeft, 'dateLeft'),
|
|
584
|
+
toPlainDate(dateRight, 'dateRight'),
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* @param {DayInput} dateLeft
|
|
591
|
+
* @param {DayInput} dateRight
|
|
592
|
+
* @returns {-1 | 0 | 1}
|
|
593
|
+
*/
|
|
594
|
+
export function compareDesc(dateLeft, dateRight) {
|
|
595
|
+
return /** @type {-1 | 0 | 1} */ (-compareAsc(dateLeft, dateRight))
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* @param {DayInput[]} dates
|
|
600
|
+
* @returns {string}
|
|
601
|
+
*/
|
|
602
|
+
export function min(dates) {
|
|
603
|
+
assertNonEmptyDates(dates)
|
|
604
|
+
return toDayString(
|
|
605
|
+
dates
|
|
606
|
+
.map((d) => toPlainDate(d))
|
|
607
|
+
.reduce((a, b) => (Temporal.PlainDate.compare(a, b) <= 0 ? a : b)),
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* @param {DayInput[]} dates
|
|
613
|
+
* @returns {string}
|
|
614
|
+
*/
|
|
615
|
+
export function max(dates) {
|
|
616
|
+
assertNonEmptyDates(dates)
|
|
617
|
+
return toDayString(
|
|
618
|
+
dates
|
|
619
|
+
.map((d) => toPlainDate(d))
|
|
620
|
+
.reduce((a, b) => (Temporal.PlainDate.compare(a, b) >= 0 ? a : b)),
|
|
621
|
+
)
|
|
622
|
+
}
|
|
623
|
+
|
|
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')
|
|
682
|
+
}
|
|
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 })
|
|
689
|
+
}
|
|
690
|
+
return out
|
|
691
|
+
}
|
|
692
|
+
|
|
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')
|
|
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
|
+
)
|
|
4
797
|
}
|
package/package.json
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "daymath",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Calendar date math (YYYY-MM-DD / PlainDate). date-fns-shaped. No time zones.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
}
|
|
9
13
|
},
|
|
10
14
|
"files": [
|
|
11
15
|
"index.js",
|
|
16
|
+
"index.d.ts",
|
|
12
17
|
"README.md"
|
|
13
18
|
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test test.js",
|
|
21
|
+
"prepublishOnly": "npm test"
|
|
22
|
+
},
|
|
14
23
|
"keywords": [
|
|
15
24
|
"date",
|
|
16
25
|
"calendar",
|
|
17
26
|
"plain-date",
|
|
18
27
|
"temporal",
|
|
19
|
-
"date-fns"
|
|
28
|
+
"date-fns",
|
|
29
|
+
"YYYY-MM-DD"
|
|
20
30
|
],
|
|
21
31
|
"author": "leemr",
|
|
22
32
|
"license": "MIT",
|
|
@@ -27,5 +37,11 @@
|
|
|
27
37
|
"bugs": {
|
|
28
38
|
"url": "https://github.com/leemr/daymath/issues"
|
|
29
39
|
},
|
|
30
|
-
"homepage": "https://github.com/leemr/daymath#readme"
|
|
40
|
+
"homepage": "https://github.com/leemr/daymath#readme",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"temporal-polyfill": "^1.0.3"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
}
|
|
31
47
|
}
|