daymath 0.0.1 → 0.1.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 +51 -6
- package/index.d.ts +37 -0
- package/index.js +276 -3
- package/package.json +20 -4
package/README.md
CHANGED
|
@@ -1,18 +1,63 @@
|
|
|
1
1
|
# daymath
|
|
2
2
|
|
|
3
|
-
Calendar date math
|
|
3
|
+
Calendar date math for `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
|
+
parse,
|
|
24
|
+
} from 'daymath'
|
|
25
|
+
|
|
26
|
+
addDays('2026-08-06', 1) // '2026-08-07'
|
|
27
|
+
addMonths('2026-01-31', 1) // '2026-02-28' (constrain)
|
|
28
|
+
differenceInDays('2026-08-06', '2026-08-01') // 5
|
|
29
|
+
isBefore('2026-08-05', '2026-08-06') // true
|
|
30
|
+
parse('2026-08-06') // '2026-08-06'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Inputs: `YYYY-MM-DD` string or `Temporal.PlainDate`.
|
|
34
|
+
Outputs: always `YYYY-MM-DD` string (for math helpers).
|
|
35
|
+
|
|
36
|
+
`Date` throws. Time-bearing ISO strings throw. Sloppy forms like `2026-8-6` throw.
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
| Function | Notes |
|
|
41
|
+
|----------|--------|
|
|
42
|
+
| `parse` / `format` / `isValid` | Strict `YYYY-MM-DD` |
|
|
43
|
+
| `addDays` / `subDays` | |
|
|
44
|
+
| `addWeeks` / `subWeeks` | |
|
|
45
|
+
| `addMonths` / `subMonths` | Calendar months; end-of-month constrains |
|
|
46
|
+
| `addYears` / `subYears` | Leap day constrains |
|
|
47
|
+
| `differenceInDays` | `dateLeft − dateRight` (date-fns order) |
|
|
48
|
+
| `isBefore` / `isAfter` / `isEqual` | |
|
|
49
|
+
| `compareAsc` / `compareDesc` | For `.sort()` |
|
|
50
|
+
| `min` / `max` | Non-empty arrays |
|
|
51
|
+
|
|
52
|
+
Amounts must be finite integers.
|
|
53
|
+
|
|
54
|
+
## Temporal
|
|
55
|
+
|
|
56
|
+
Uses global `Temporal` when present; otherwise [`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill).
|
|
57
|
+
|
|
58
|
+
## Types
|
|
59
|
+
|
|
60
|
+
Ships `index.d.ts` (no TypeScript compile step for authors). Source is plain JS.
|
|
16
61
|
|
|
17
62
|
## License
|
|
18
63
|
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Temporal } from 'temporal-polyfill'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calendar day input: `YYYY-MM-DD` string, or a Temporal.PlainDate.
|
|
5
|
+
* `Date` is rejected at runtime.
|
|
6
|
+
*/
|
|
7
|
+
export type DayInput = string | Temporal.PlainDate
|
|
8
|
+
|
|
9
|
+
export function isValid(value: unknown): boolean
|
|
10
|
+
|
|
11
|
+
/** Validate / normalize to `YYYY-MM-DD`. */
|
|
12
|
+
export function parse(date: DayInput): string
|
|
13
|
+
|
|
14
|
+
/** Format as `YYYY-MM-DD` (only pattern supported). */
|
|
15
|
+
export function format(date: DayInput, pattern?: 'yyyy-MM-dd' | 'YYYY-MM-DD'): string
|
|
16
|
+
|
|
17
|
+
export function addDays(date: DayInput, amount: number): string
|
|
18
|
+
export function subDays(date: DayInput, amount: number): string
|
|
19
|
+
export function addWeeks(date: DayInput, amount: number): string
|
|
20
|
+
export function subWeeks(date: DayInput, amount: number): string
|
|
21
|
+
export function addMonths(date: DayInput, amount: number): string
|
|
22
|
+
export function subMonths(date: DayInput, amount: number): string
|
|
23
|
+
export function addYears(date: DayInput, amount: number): string
|
|
24
|
+
export function subYears(date: DayInput, amount: number): string
|
|
25
|
+
|
|
26
|
+
/** Full days: `dateLeft − dateRight` (date-fns argument order). */
|
|
27
|
+
export function differenceInDays(dateLeft: DayInput, dateRight: DayInput): number
|
|
28
|
+
|
|
29
|
+
export function isBefore(date: DayInput, dateToCompare: DayInput): boolean
|
|
30
|
+
export function isAfter(date: DayInput, dateToCompare: DayInput): boolean
|
|
31
|
+
export function isEqual(dateLeft: DayInput, dateRight: DayInput): boolean
|
|
32
|
+
|
|
33
|
+
export function compareAsc(dateLeft: DayInput, dateRight: DayInput): -1 | 0 | 1
|
|
34
|
+
export function compareDesc(dateLeft: DayInput, dateRight: DayInput): -1 | 0 | 1
|
|
35
|
+
|
|
36
|
+
export function min(dates: DayInput[]): string
|
|
37
|
+
export function max(dates: DayInput[]): string
|
package/index.js
CHANGED
|
@@ -1,4 +1,277 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Reject Date and non-calendar values. Accept YYYY-MM-DD string or PlainDate.
|
|
10
|
+
* @param {unknown} value
|
|
11
|
+
* @param {string} label
|
|
12
|
+
* @returns {Temporal.PlainDate}
|
|
13
|
+
*/
|
|
14
|
+
function toPlainDate(value, label = 'date') {
|
|
15
|
+
if (value instanceof Date) {
|
|
16
|
+
throw new TypeError(
|
|
17
|
+
`daymath: Date is not allowed for ${label} (pass YYYY-MM-DD string)`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'string') {
|
|
21
|
+
// Strict calendar day: no time, no offset, no week dates.
|
|
22
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
23
|
+
throw new RangeError(
|
|
24
|
+
`daymath: ${label} must be YYYY-MM-DD (got ${JSON.stringify(value)})`,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return Temporal.PlainDate.from(value)
|
|
29
|
+
} catch (err) {
|
|
30
|
+
throw new RangeError(`daymath: invalid ${label} ${JSON.stringify(value)}`, {
|
|
31
|
+
cause: err,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (value instanceof Temporal.PlainDate) {
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
38
|
+
throw new TypeError(
|
|
39
|
+
`daymath: ${label} must be YYYY-MM-DD string or Temporal.PlainDate`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {Temporal.PlainDate} plain @returns {string} */
|
|
44
|
+
function toDayString(plain) {
|
|
45
|
+
return plain.toString()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {unknown} value */
|
|
49
|
+
export function isValid(value) {
|
|
50
|
+
try {
|
|
51
|
+
toPlainDate(value)
|
|
52
|
+
return true
|
|
53
|
+
} catch {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate / normalize a calendar day string.
|
|
60
|
+
* @param {DayInput} date
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function parse(date) {
|
|
64
|
+
return toDayString(toPlainDate(date))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format as YYYY-MM-DD (only supported pattern for now).
|
|
69
|
+
* @param {DayInput} date
|
|
70
|
+
* @param {string} [pattern='yyyy-MM-dd']
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function format(date, pattern = 'yyyy-MM-dd') {
|
|
74
|
+
if (pattern !== 'yyyy-MM-dd' && pattern !== 'YYYY-MM-DD') {
|
|
75
|
+
throw new RangeError(
|
|
76
|
+
`daymath: only "yyyy-MM-dd" format is supported (got ${JSON.stringify(pattern)})`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
return toDayString(toPlainDate(date))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {DayInput} date
|
|
84
|
+
* @param {number} amount
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
export function addDays(date, amount) {
|
|
88
|
+
assertFiniteNumber(amount, 'amount')
|
|
89
|
+
return toDayString(toPlainDate(date).add({ days: amount }))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {DayInput} date
|
|
94
|
+
* @param {number} amount
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export function subDays(date, amount) {
|
|
98
|
+
assertFiniteNumber(amount, 'amount')
|
|
99
|
+
return addDays(date, -amount)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {DayInput} date
|
|
104
|
+
* @param {number} amount
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
export function addWeeks(date, amount) {
|
|
108
|
+
assertFiniteNumber(amount, 'amount')
|
|
109
|
+
return addDays(date, amount * 7)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {DayInput} date
|
|
114
|
+
* @param {number} amount
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function subWeeks(date, amount) {
|
|
118
|
+
assertFiniteNumber(amount, 'amount')
|
|
119
|
+
return addWeeks(date, -amount)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Calendar months (Temporal overflow: constrain — e.g. Jan 31 + 1 month → Feb 28/29).
|
|
124
|
+
* @param {DayInput} date
|
|
125
|
+
* @param {number} amount
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function addMonths(date, amount) {
|
|
129
|
+
assertFiniteNumber(amount, 'amount')
|
|
130
|
+
return toDayString(toPlainDate(date).add({ months: amount }))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {DayInput} date
|
|
135
|
+
* @param {number} amount
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
export function subMonths(date, amount) {
|
|
139
|
+
assertFiniteNumber(amount, 'amount')
|
|
140
|
+
return addMonths(date, -amount)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {DayInput} date
|
|
145
|
+
* @param {number} amount
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function addYears(date, amount) {
|
|
149
|
+
assertFiniteNumber(amount, 'amount')
|
|
150
|
+
return toDayString(toPlainDate(date).add({ years: amount }))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {DayInput} date
|
|
155
|
+
* @param {number} amount
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
export function subYears(date, amount) {
|
|
159
|
+
assertFiniteNumber(amount, 'amount')
|
|
160
|
+
return addYears(date, -amount)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Full calendar days: dateLeft − dateRight (date-fns order).
|
|
165
|
+
* @param {DayInput} dateLeft
|
|
166
|
+
* @param {DayInput} dateRight
|
|
167
|
+
* @returns {number}
|
|
168
|
+
*/
|
|
169
|
+
export function differenceInDays(dateLeft, dateRight) {
|
|
170
|
+
const left = toPlainDate(dateLeft, 'dateLeft')
|
|
171
|
+
const right = toPlainDate(dateRight, 'dateRight')
|
|
172
|
+
return left.since(right, { largestUnit: 'day' }).days
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {DayInput} date
|
|
177
|
+
* @param {DayInput} dateToCompare
|
|
178
|
+
* @returns {boolean}
|
|
179
|
+
*/
|
|
180
|
+
export function isBefore(date, dateToCompare) {
|
|
181
|
+
return (
|
|
182
|
+
Temporal.PlainDate.compare(
|
|
183
|
+
toPlainDate(date),
|
|
184
|
+
toPlainDate(dateToCompare, 'dateToCompare'),
|
|
185
|
+
) < 0
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {DayInput} date
|
|
191
|
+
* @param {DayInput} dateToCompare
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
export function isAfter(date, dateToCompare) {
|
|
195
|
+
return (
|
|
196
|
+
Temporal.PlainDate.compare(
|
|
197
|
+
toPlainDate(date),
|
|
198
|
+
toPlainDate(dateToCompare, 'dateToCompare'),
|
|
199
|
+
) > 0
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {DayInput} dateLeft
|
|
205
|
+
* @param {DayInput} dateRight
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
export function isEqual(dateLeft, dateRight) {
|
|
209
|
+
return (
|
|
210
|
+
Temporal.PlainDate.compare(
|
|
211
|
+
toPlainDate(dateLeft, 'dateLeft'),
|
|
212
|
+
toPlainDate(dateRight, 'dateRight'),
|
|
213
|
+
) === 0
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {DayInput} dateLeft
|
|
219
|
+
* @param {DayInput} dateRight
|
|
220
|
+
* @returns {-1 | 0 | 1}
|
|
221
|
+
*/
|
|
222
|
+
export function compareAsc(dateLeft, dateRight) {
|
|
223
|
+
return /** @type {-1 | 0 | 1} */ (
|
|
224
|
+
Temporal.PlainDate.compare(
|
|
225
|
+
toPlainDate(dateLeft, 'dateLeft'),
|
|
226
|
+
toPlainDate(dateRight, 'dateRight'),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {DayInput} dateLeft
|
|
233
|
+
* @param {DayInput} dateRight
|
|
234
|
+
* @returns {-1 | 0 | 1}
|
|
235
|
+
*/
|
|
236
|
+
export function compareDesc(dateLeft, dateRight) {
|
|
237
|
+
return /** @type {-1 | 0 | 1} */ (-compareAsc(dateLeft, dateRight))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {DayInput[]} dates
|
|
242
|
+
* @returns {string}
|
|
243
|
+
*/
|
|
244
|
+
export function min(dates) {
|
|
245
|
+
assertNonEmptyDates(dates)
|
|
246
|
+
return toDayString(
|
|
247
|
+
dates.map((d) => toPlainDate(d)).reduce((a, b) => (Temporal.PlainDate.compare(a, b) <= 0 ? a : b)),
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {DayInput[]} dates
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
export function max(dates) {
|
|
256
|
+
assertNonEmptyDates(dates)
|
|
257
|
+
return toDayString(
|
|
258
|
+
dates.map((d) => toPlainDate(d)).reduce((a, b) => (Temporal.PlainDate.compare(a, b) >= 0 ? a : b)),
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** @param {unknown} n @param {string} label */
|
|
263
|
+
function assertFiniteNumber(n, label) {
|
|
264
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) {
|
|
265
|
+
throw new TypeError(`daymath: ${label} must be a finite number`)
|
|
266
|
+
}
|
|
267
|
+
if (!Number.isInteger(n)) {
|
|
268
|
+
throw new RangeError(`daymath: ${label} must be an integer`)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** @param {unknown} dates */
|
|
273
|
+
function assertNonEmptyDates(dates) {
|
|
274
|
+
if (!Array.isArray(dates) || dates.length === 0) {
|
|
275
|
+
throw new RangeError('daymath: expected a non-empty array of dates')
|
|
276
|
+
}
|
|
4
277
|
}
|
package/package.json
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "daymath",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.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
|
}
|