daymath 0.6.0 → 0.7.1
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 +28 -8
- package/index.d.ts +259 -14
- package/index.js +91 -16
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -42,8 +42,8 @@ that reads a clock.
|
|
|
42
42
|
Start here. No arguments means today, in UTC.
|
|
43
43
|
|
|
44
44
|
```js
|
|
45
|
-
day() //
|
|
46
|
-
addDays(day(), 2) //
|
|
45
|
+
day() // today, in UTC
|
|
46
|
+
addDays(day(), 2) // two days from today
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Then hand it whatever you already have. It converts; it never stores what you gave it.
|
|
@@ -53,12 +53,29 @@ day(row.createdAt) // '2026-08-07' a Date
|
|
|
53
53
|
day(1761616161771) // '2025-10-28' epoch milliseconds
|
|
54
54
|
day('1999-01-01T00:00:00Z') // '1999-01-01' an ISO timestamp
|
|
55
55
|
day('2026-05-05') // '2026-05-05' already a day
|
|
56
|
+
day('2026-08-08 12:00:00') // '2026-08-08' a SQLite DATETIME
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
A **SQLite `DATETIME` goes straight in**, and so does the whole surface — the clock
|
|
60
|
+
is dropped by the same funnel every export shares, so this is not a `day()` feature.
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
addDays(row.created_at, 30) // '2026-09-07' from '2026-08-08 12:00:00'
|
|
64
|
+
startOfMonth('2026-08-08 01:57:31.913') // '2026-08-01' strftime('%…%H:%M:%f')
|
|
65
|
+
getYear('2026-08-08t12:00') // 2026 T, t or a space; same rule
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`datetime()`, `CURRENT_TIMESTAMP` and `strftime('%Y-%m-%d %H:%M:%f')` all emit that
|
|
69
|
+
shape, so a column value needs no reshaping first. **Pass a zone with one and it
|
|
70
|
+
throws.** Naming a zone means convert, and a clock with no zone gives nothing to
|
|
71
|
+
convert from — and `datetime()` defaults to UTC, so a silent answer would hand the
|
|
72
|
+
UTC day to the one caller who asked for a local one. Put the zone in the string
|
|
73
|
+
(`Z`, an offset, or `[Zone]`) and it converts normally.
|
|
74
|
+
|
|
58
75
|
Name a zone when the answer depends on one.
|
|
59
76
|
|
|
60
77
|
```js
|
|
61
|
-
day('Asia/Tokyo') //
|
|
78
|
+
day('Asia/Tokyo') // today in Tokyo
|
|
62
79
|
day(row.createdAt, 'America/New_York') // '2026-08-06' the evening before
|
|
63
80
|
day('2026-08-08T23:00:00Z', 'Asia/Tokyo') // '2026-08-09' same instant, next day
|
|
64
81
|
day(zdt.toString()) // the zone in the string wins
|
|
@@ -69,7 +86,7 @@ UTC is still not your day for part of every day — it runs ahead of `America/Ne
|
|
|
69
86
|
for 16.7% of the day, and behind `Asia/Tokyo` for 37.5% — so name your zone when
|
|
70
87
|
that matters.
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
Six rules worth knowing:
|
|
73
90
|
|
|
74
91
|
- A **number is epoch milliseconds**, exactly as `new Date(n)` reads it. A seconds
|
|
75
92
|
timestamp read as milliseconds lands in 1970, with no error. daymath states the
|
|
@@ -84,9 +101,12 @@ Four rules worth knowing:
|
|
|
84
101
|
the UTC default never applies. `day(zdt.toString())` equals `zdt.toPlainDate()`.
|
|
85
102
|
A browser sending `'2026-08-08T20:00:00-04:00[America/New_York]'` gets back the
|
|
86
103
|
8th, which is the date its user saw. Passing `tz` as well throws.
|
|
87
|
-
- A
|
|
88
|
-
|
|
89
|
-
the
|
|
104
|
+
- A **zoneless wall clock is a day.** `'2026-08-08 12:00:00'`, `'2026-08-08T12:00'`
|
|
105
|
+
and the lowercase `t` all answer `'2026-08-08'`; the clock is dropped, never read.
|
|
106
|
+
The **hour is the only bound**, because the hour is the only field that can change
|
|
107
|
+
the date: `24:00` is refused, a leap second and a fraction of any length are not.
|
|
108
|
+
Pass `tz` with one and it **throws**. Name the zone in the string —
|
|
109
|
+
`'2026-08-08T12:00[America/New_York]'` — and it converts.
|
|
90
110
|
- **`'11/12/2026'` is refused.** Nobody can tell November from December in it.
|
|
91
111
|
|
|
92
112
|
A lone string takes one of four roles, decided in this order: a day, a zoned time,
|
|
@@ -194,7 +214,7 @@ For the accepting family only the year label moves, so every export still answer
|
|
|
194
214
|
the annotation rides along:
|
|
195
215
|
|
|
196
216
|
```js
|
|
197
|
-
getYear('2026-01-31[u-ca=buddhist]') // 2569
|
|
217
|
+
getYear('2026-01-31[u-ca=buddhist]') // 2569 not 2026
|
|
198
218
|
getMonth('2026-01-31[u-ca=buddhist]') // 1
|
|
199
219
|
addDays('2026-01-31[u-ca=buddhist]', 1) // '2026-02-01[u-ca=buddhist]'
|
|
200
220
|
setYear('2026-01-31[u-ca=buddhist]', 2570) // '2027-01-31[u-ca=buddhist]'
|
package/index.d.ts
CHANGED
|
@@ -4,6 +4,17 @@ import type { Temporal } from 'temporal-polyfill'
|
|
|
4
4
|
* Calendar day input: ISO 8601 day string, or a Temporal.PlainDate.
|
|
5
5
|
* - `YYYY-MM-DD` (years 0000–9999)
|
|
6
6
|
* - expanded `±YYYYYY-MM-DD` (e.g. `+010000-01-01`)
|
|
7
|
+
* - either of those with a zoneless wall clock on it, `YYYY-MM-DD HH:MM[:SS[.fff]]`,
|
|
8
|
+
* with `T`, `t` or a space between them. The clock is dropped and never read, so
|
|
9
|
+
* `getYear('2026-08-08 12:00:00')` is `2026`. This is the shape SQLite stores:
|
|
10
|
+
* `datetime()`, `CURRENT_TIMESTAMP` and `strftime('%Y-%m-%d %H:%M:%f')` all emit
|
|
11
|
+
* it, so a column value needs no reshaping. The **hour is the only bound**,
|
|
12
|
+
* because the hour is the only field that can change the date: `24:00` is refused
|
|
13
|
+
* because ISO `24:00` starts the next day, while a leap second `23:59:60` and a
|
|
14
|
+
* fraction of any length stay inside their own day and are accepted.
|
|
15
|
+
*
|
|
16
|
+
* A clock naming a zone — `Z`, an offset, or `[Zone]` — is NOT day input. It names
|
|
17
|
+
* an instant, and `day()` is the only door an instant enters by.
|
|
7
18
|
*
|
|
8
19
|
* Usable range is the Temporal `PlainDate` range `-271821-04-19` …
|
|
9
20
|
* `+275760-09-13`; a day outside it throws a `RangeError`.
|
|
@@ -50,8 +61,12 @@ export type WeekOptions = {
|
|
|
50
61
|
* own civil day and the UTC default never applies. Passing `tz` as well throws.
|
|
51
62
|
*
|
|
52
63
|
* `'11/12/2026'` is refused, because nobody can tell November from December in
|
|
53
|
-
* it.
|
|
54
|
-
*
|
|
64
|
+
* it.
|
|
65
|
+
*
|
|
66
|
+
* A zoneless wall clock is a day, so `'2026-08-08 12:00:00'` — a SQLite
|
|
67
|
+
* `DATETIME` — answers `'2026-08-08'` and the clock is dropped. Pass `tz` with
|
|
68
|
+
* one and it throws: naming a zone means convert, and a clock with no zone gives
|
|
69
|
+
* nothing to convert from. Put the zone in the string and it converts.
|
|
55
70
|
*
|
|
56
71
|
* A lone string takes one of four roles, in this order: a day, a zoned time, an
|
|
57
72
|
* instant, then a zone. The zone test is by shape — an IANA name, which carries
|
|
@@ -65,121 +80,351 @@ export type WeekOptions = {
|
|
|
65
80
|
*
|
|
66
81
|
* A calendar that renumbers months or days is still refused where it is
|
|
67
82
|
* applied, which means with a `[Zone]` bracket and on a day string.
|
|
83
|
+
* @example day(row.createdAt) // a Date, read in UTC
|
|
84
|
+
* @example day(row.createdAt, 'Asia/Tokyo') // the same instant, Tokyo's day
|
|
85
|
+
* @example day('1999-01-01T00:00:00Z') // '1999-01-01'
|
|
86
|
+
* @example day('2026-08-08 12:00:00') // '2026-08-08' a SQLite DATETIME
|
|
87
|
+
* @example day('2026-05-05', 'Asia/Tokyo') // '2026-05-05' a day carries no time
|
|
68
88
|
*/
|
|
69
|
-
export function day(tz?: string): string
|
|
70
89
|
export function day(
|
|
71
90
|
moment: Date | number | DayInput | null | undefined,
|
|
72
91
|
tz?: string,
|
|
73
92
|
): string
|
|
93
|
+
/**
|
|
94
|
+
* Today's calendar day, in a zone. **This is the one overload that reads a clock**, so it is the
|
|
95
|
+
* only call in daymath that is not a pure function. Give the other overload a moment and it is.
|
|
96
|
+
*
|
|
97
|
+
* The zone defaults to UTC, and the default is STATED rather than assumed: daymath has no silent
|
|
98
|
+
* local now. UTC is not your day for part of every day — it runs ahead of `America/New_York` for
|
|
99
|
+
* 16.7% of the day and behind `Asia/Tokyo` for 37.5% — so name your zone when that matters.
|
|
100
|
+
*
|
|
101
|
+
* A lone string here is read as a zone only after it fails to be a day, a zoned time and an
|
|
102
|
+
* instant, in that order. The zone test is by shape, so a timestamp can never be read as a zone.
|
|
103
|
+
* @example day() // today, in UTC
|
|
104
|
+
* @example day('Asia/Tokyo') // today in Tokyo
|
|
105
|
+
* @example day('+05:30') // a bare offset is a zone too
|
|
106
|
+
*/
|
|
107
|
+
export function day(tz?: string): string
|
|
74
108
|
|
|
75
109
|
/**
|
|
76
110
|
* True for a valid daymath day string / PlainDate.
|
|
77
111
|
* Invalid strings → false. `Date` → throws TypeError (not a quiet false).
|
|
112
|
+
* @example isValid('2026-08-08') // true
|
|
113
|
+
* @example isValid('2026-02-30') // false
|
|
114
|
+
* @example isValid('2026-08-08 12:00:00') // true clock dropped
|
|
115
|
+
* @example isValid(new Date()) // throws TypeError, not false
|
|
78
116
|
*/
|
|
79
117
|
export function isValid(value: unknown): boolean
|
|
80
118
|
|
|
81
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Validate / normalize to ISO 8601 day string (Temporal `toString` form).
|
|
121
|
+
*
|
|
122
|
+
* `parse` preserves what it is given; `day()` normalizes. So a calendar
|
|
123
|
+
* annotation survives here and is dropped by `day()`.
|
|
124
|
+
* @example parse('2026-08-08') // '2026-08-08'
|
|
125
|
+
* @example parse('2026-08-08 12:00:00') // '2026-08-08' clock dropped
|
|
126
|
+
* @example parse('2026-01-31[u-ca=buddhist]') // '2026-01-31[u-ca=buddhist]'
|
|
127
|
+
* @example parse('2026-02-30') // throws RangeError
|
|
128
|
+
*/
|
|
82
129
|
export function parse(date: DayInput): string
|
|
83
130
|
|
|
84
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Format as ISO day (only `yyyy-MM-dd` / `YYYY-MM-DD` patterns supported).
|
|
133
|
+
*
|
|
134
|
+
* Not locale display. For that, use `Intl` or a date-fns formatter.
|
|
135
|
+
* @example format('2026-08-08') // '2026-08-08'
|
|
136
|
+
* @example format('2026-08-08', 'dd/MM/yyyy') // throws RangeError
|
|
137
|
+
*/
|
|
85
138
|
export function format(date: DayInput, pattern?: 'yyyy-MM-dd' | 'YYYY-MM-DD'): string
|
|
86
139
|
|
|
140
|
+
/**
|
|
141
|
+
* @example addDays('2026-08-08', 30) // '2026-09-07'
|
|
142
|
+
* @example addDays('2026-08-08', -1) // '2026-08-07'
|
|
143
|
+
*/
|
|
87
144
|
export function addDays(date: DayInput, amount: number): string
|
|
145
|
+
/** @example subDays('2026-08-08', 1) // '2026-08-07' */
|
|
88
146
|
export function subDays(date: DayInput, amount: number): string
|
|
147
|
+
/** @example addWeeks('2026-08-08', 2) // '2026-08-22' */
|
|
89
148
|
export function addWeeks(date: DayInput, amount: number): string
|
|
149
|
+
/** Whole weeks. @example subWeeks('2026-08-22', 2) // '2026-08-08' */
|
|
90
150
|
export function subWeeks(date: DayInput, amount: number): string
|
|
151
|
+
/**
|
|
152
|
+
* Overflow **clamps** to the last day of the target month; it never rolls into
|
|
153
|
+
* the next one. Same rule in both directions, and the same rule `setMonth`,
|
|
154
|
+
* `setDate` and `setYear` use.
|
|
155
|
+
* @example addMonths('2026-01-31', 1) // '2026-02-28' clamped, not '2026-03-03'
|
|
156
|
+
* @example addMonths('2026-03-31', -1) // '2026-02-28'
|
|
157
|
+
* @example addMonths('2024-01-31', 1) // '2024-02-29' leap year
|
|
158
|
+
*/
|
|
91
159
|
export function addMonths(date: DayInput, amount: number): string
|
|
160
|
+
/**
|
|
161
|
+
* Clamps, exactly as `addMonths` does.
|
|
162
|
+
* @example subMonths('2026-03-31', 1) // '2026-02-28' clamped
|
|
163
|
+
*/
|
|
92
164
|
export function subMonths(date: DayInput, amount: number): string
|
|
165
|
+
/**
|
|
166
|
+
* Clamps, so 29 February lands on the 28th of a common year.
|
|
167
|
+
* @example addYears('2024-02-29', 1) // '2025-02-28' clamped
|
|
168
|
+
*/
|
|
93
169
|
export function addYears(date: DayInput, amount: number): string
|
|
170
|
+
/**
|
|
171
|
+
* Clamps, exactly as `addYears` does.
|
|
172
|
+
* @example subYears('2024-02-29', 1) // '2023-02-28' clamped
|
|
173
|
+
*/
|
|
94
174
|
export function subYears(date: DayInput, amount: number): string
|
|
175
|
+
/**
|
|
176
|
+
* Three months at a time, and it clamps.
|
|
177
|
+
* @example addQuarters('2026-01-31', 1) // '2026-04-30' clamped
|
|
178
|
+
*/
|
|
95
179
|
export function addQuarters(date: DayInput, amount: number): string
|
|
180
|
+
/** Three months back, and it clamps. @example subQuarters('2026-04-30', 1) // '2026-01-30' */
|
|
96
181
|
export function subQuarters(date: DayInput, amount: number): string
|
|
97
182
|
|
|
98
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Full year number. An annotation is applied on top of the ISO date part, never instead of it.
|
|
185
|
+
* @example getYear('2026-08-08') // 2026
|
|
186
|
+
* @example getYear('2026-01-31[u-ca=buddhist]') // 2569 the annotation adds 543
|
|
187
|
+
*/
|
|
99
188
|
export function getYear(date: DayInput): number
|
|
100
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Month number, ISO 8601: 1 = January … 12 = December. Not date-fns's 0-based index.
|
|
191
|
+
* @example getMonth('2026-08-08') // 8 — date-fns would answer 7
|
|
192
|
+
* @example getMonth('2026-01-15') // 1
|
|
193
|
+
*/
|
|
101
194
|
export function getMonth(date: DayInput): number
|
|
102
|
-
/** Day of month 1…31. */
|
|
195
|
+
/** Day of month 1…31. @example getDate('2026-08-08') // 8 */
|
|
103
196
|
export function getDate(date: DayInput): number
|
|
104
|
-
/**
|
|
197
|
+
/**
|
|
198
|
+
* Weekday, ISO 8601: 1 = Monday … 7 = Sunday. Only Sunday differs from date-fns.
|
|
199
|
+
* @example getDay('2026-08-10') // 1 a Monday
|
|
200
|
+
* @example getDay('2026-08-09') // 7 a Sunday — date-fns would answer 0
|
|
201
|
+
*/
|
|
105
202
|
export function getDay(date: DayInput): number
|
|
203
|
+
/** 1 on 1 January. @example getDayOfYear('2026-03-01') // 60 */
|
|
106
204
|
export function getDayOfYear(date: DayInput): number
|
|
205
|
+
/** @example getDaysInMonth('2024-02-10') // 29 */
|
|
107
206
|
export function getDaysInMonth(date: DayInput): number
|
|
108
|
-
/** Quarter 1…4. */
|
|
207
|
+
/** Quarter 1…4. @example getQuarter('2026-08-08') // 3 */
|
|
109
208
|
export function getQuarter(date: DayInput): number
|
|
209
|
+
/**
|
|
210
|
+
* Leap year of the day's own ISO year.
|
|
211
|
+
*
|
|
212
|
+
* **This is the one way to get a wrong answer with no error**, and it is worth reading before
|
|
213
|
+
* you pass a non-ISO calendar's year. A `[u-ca=…]` day is judged on its ISO year, which is the
|
|
214
|
+
* date part — so a Buddhist year written as a bare ISO year is a different year. Buddhist 2567
|
|
215
|
+
* is ISO 2024 and IS a leap year, but 2567 read as ISO is not, because 543 mod 4 is 3. The two
|
|
216
|
+
* rules disagree in 49 of the 101 Buddhist years from 2500 to 2600, and nothing throws.
|
|
217
|
+
* @example isLeapYear('2024-01-01') // true
|
|
218
|
+
* @example isLeapYear('2567-01-01') // false ISO 2567, NOT Buddhist 2567 (which is ISO 2024)
|
|
219
|
+
*/
|
|
110
220
|
export function isLeapYear(date: DayInput): boolean
|
|
111
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Overflow clamps, so 29 February survives a move to a common year. **date-fns rolls here
|
|
224
|
+
* instead**, so this is the second deliberate disagreement after `setDate` — and the one a caller
|
|
225
|
+
* reaches by accident, through a stored 29 February.
|
|
226
|
+
* @example setYear('2024-02-29', 2026) // '2026-02-28' clamped; date-fns gives '2026-03-01'
|
|
227
|
+
*/
|
|
112
228
|
export function setYear(date: DayInput, year: number): string
|
|
113
|
-
/**
|
|
229
|
+
/**
|
|
230
|
+
* `month`: 1 = January … 12 = December (ISO 8601). Overflow clamps.
|
|
231
|
+
* @example setMonth('2026-01-31', 2) // '2026-02-28' clamped
|
|
232
|
+
*/
|
|
114
233
|
export function setMonth(date: DayInput, month: number): string
|
|
234
|
+
/**
|
|
235
|
+
* Overflow clamps to the end of the month. **date-fns rolls here instead**, so
|
|
236
|
+
* this is one of the few places the two answer differently on purpose.
|
|
237
|
+
* @example setDate('2026-02-01', 31) // '2026-02-28' daymath clamps
|
|
238
|
+
*/
|
|
115
239
|
export function setDate(date: DayInput, dayOfMonth: number): string
|
|
116
240
|
|
|
241
|
+
/** @example startOfMonth('2026-08-08') // '2026-08-01' */
|
|
117
242
|
export function startOfMonth(date: DayInput): string
|
|
243
|
+
/**
|
|
244
|
+
* @example endOfMonth('2024-02-01') // '2024-02-29'
|
|
245
|
+
*/
|
|
118
246
|
export function endOfMonth(date: DayInput): string
|
|
247
|
+
/** @example startOfYear('2026-08-08') // '2026-01-01' */
|
|
119
248
|
export function startOfYear(date: DayInput): string
|
|
249
|
+
/** @example endOfYear('2026-08-08') // '2026-12-31' */
|
|
120
250
|
export function endOfYear(date: DayInput): string
|
|
251
|
+
/** @example startOfQuarter('2026-08-08') // '2026-07-01' */
|
|
121
252
|
export function startOfQuarter(date: DayInput): string
|
|
253
|
+
/** @example endOfQuarter('2026-08-08') // '2026-09-30' */
|
|
122
254
|
export function endOfQuarter(date: DayInput): string
|
|
255
|
+
/**
|
|
256
|
+
* `weekStartsOn` defaults to **7 (Sunday)**, and `0` is accepted for Sunday too.
|
|
257
|
+
* @example startOfWeek('2026-08-12') // '2026-08-09' Sunday
|
|
258
|
+
* @example startOfWeek('2026-08-12', { weekStartsOn: 1 }) // '2026-08-10' Monday
|
|
259
|
+
*/
|
|
123
260
|
export function startOfWeek(date: DayInput, options?: WeekOptions): string
|
|
261
|
+
/**
|
|
262
|
+
* `weekStartsOn` defaults to 7 (Sunday), so the week ends on Saturday.
|
|
263
|
+
* @example endOfWeek('2026-08-12') // '2026-08-15' Saturday
|
|
264
|
+
* @example endOfWeek('2026-08-12', { weekStartsOn: 1 }) // '2026-08-16' Sunday
|
|
265
|
+
*/
|
|
124
266
|
export function endOfWeek(date: DayInput, options?: WeekOptions): string
|
|
125
267
|
|
|
126
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* Full days: `dateLeft − dateRight` (date-fns argument order).
|
|
270
|
+
* @example differenceInDays('2026-08-08', '2026-08-01') // 7
|
|
271
|
+
* @example differenceInDays('2026-08-01', '2026-08-08') // -7 order matters
|
|
272
|
+
*/
|
|
127
273
|
export function differenceInDays(dateLeft: DayInput, dateRight: DayInput): number
|
|
274
|
+
/** Whole weeks. @example differenceInWeeks('2026-08-15', '2026-08-08') // 1 */
|
|
128
275
|
export function differenceInWeeks(dateLeft: DayInput, dateRight: DayInput): number
|
|
276
|
+
/**
|
|
277
|
+
* A month counts as full when `addMonths` would carry the earlier date to the
|
|
278
|
+
* later one, so `differenceInMonths(addMonths(d, n), d) === n` always holds. Use
|
|
279
|
+
* `differenceInCalendarMonths` to count boundaries crossed instead.
|
|
280
|
+
* @example differenceInMonths('2026-02-28', '2026-01-31') // 1 addMonths clamps to here
|
|
281
|
+
* @example differenceInCalendarMonths('2026-02-01', '2026-01-31') // 1 one boundary, one day apart
|
|
282
|
+
*/
|
|
129
283
|
export function differenceInMonths(dateLeft: DayInput, dateRight: DayInput): number
|
|
284
|
+
/**
|
|
285
|
+
* Month boundaries crossed, not whole months. `differenceInMonths` is the other question.
|
|
286
|
+
* @example differenceInCalendarMonths('2026-02-01', '2026-01-31') // 1 one day apart
|
|
287
|
+
*/
|
|
130
288
|
export function differenceInCalendarMonths(
|
|
131
289
|
dateLeft: DayInput,
|
|
132
290
|
dateRight: DayInput,
|
|
133
291
|
): number
|
|
292
|
+
/**
|
|
293
|
+
* Counts by `addYears`, the same rule `differenceInMonths` uses, so the round-trip law holds.
|
|
294
|
+
* @example differenceInYears('2026-02-28', '2024-02-29') // 2 addYears clamps to here
|
|
295
|
+
*/
|
|
134
296
|
export function differenceInYears(dateLeft: DayInput, dateRight: DayInput): number
|
|
297
|
+
/**
|
|
298
|
+
* Boundaries crossed, not whole years.
|
|
299
|
+
* @example differenceInCalendarYears('2026-01-01', '2025-12-31') // 1 one day apart
|
|
300
|
+
*/
|
|
135
301
|
export function differenceInCalendarYears(dateLeft: DayInput, dateRight: DayInput): number
|
|
302
|
+
/** Counts by `addQuarters`. @example differenceInQuarters('2026-07-01', '2026-01-01') // 2 */
|
|
136
303
|
export function differenceInQuarters(dateLeft: DayInput, dateRight: DayInput): number
|
|
304
|
+
/**
|
|
305
|
+
* Quarter boundaries crossed, not whole quarters.
|
|
306
|
+
* @example differenceInCalendarQuarters('2026-07-01', '2026-06-30') // 1 one day apart
|
|
307
|
+
*/
|
|
137
308
|
export function differenceInCalendarQuarters(
|
|
138
309
|
dateLeft: DayInput,
|
|
139
310
|
dateRight: DayInput,
|
|
140
311
|
): number
|
|
141
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Strictly before; an equal pair is `false`.
|
|
315
|
+
* @example isBefore('2026-08-01', '2026-08-08') // true
|
|
316
|
+
* @example isBefore('2026-08-08', '2026-08-08') // false not strictly before
|
|
317
|
+
*/
|
|
142
318
|
export function isBefore(date: DayInput, dateToCompare: DayInput): boolean
|
|
319
|
+
/** Strictly after. @example isAfter('2026-08-01', '2026-08-08') // false */
|
|
143
320
|
export function isAfter(date: DayInput, dateToCompare: DayInput): boolean
|
|
144
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* Same calendar day, and the calendar label is ignored. Temporal's own `equals`
|
|
323
|
+
* compares the calendar too, so it answers `false` for one day written two ways;
|
|
324
|
+
* daymath compares the day alone and answers `true`.
|
|
325
|
+
* @example isEqual('2026-01-31[u-ca=buddhist]', '2026-01-31') // true same day
|
|
326
|
+
*/
|
|
145
327
|
export function isEqual(dateLeft: DayInput, dateRight: DayInput): boolean
|
|
146
|
-
/**
|
|
328
|
+
/**
|
|
329
|
+
* Alias of `isEqual` (date-fns name), and it ignores the calendar label the same way.
|
|
330
|
+
* @example isSameDay('2026-01-31[u-ca=buddhist]', '2026-01-31') // true same day
|
|
331
|
+
*/
|
|
147
332
|
export const isSameDay: typeof isEqual
|
|
148
333
|
|
|
334
|
+
/**
|
|
335
|
+
* `weekStartsOn` defaults to 7 (Sunday), so a Saturday and the Sunday after it are DIFFERENT weeks.
|
|
336
|
+
* @example isSameWeek('2026-08-08', '2026-08-09') // false Sat then Sun
|
|
337
|
+
* @example isSameWeek('2026-08-08', '2026-08-09', { weekStartsOn: 1 }) // true Monday weeks
|
|
338
|
+
*/
|
|
149
339
|
export function isSameWeek(
|
|
150
340
|
dateLeft: DayInput,
|
|
151
341
|
dateRight: DayInput,
|
|
152
342
|
options?: WeekOptions,
|
|
153
343
|
): boolean
|
|
344
|
+
/** Same month AND year. @example isSameMonth('2026-08-01', '2026-08-31') // true */
|
|
154
345
|
export function isSameMonth(dateLeft: DayInput, dateRight: DayInput): boolean
|
|
346
|
+
/** @example isSameYear('2026-01-01', '2026-12-31') // true */
|
|
155
347
|
export function isSameYear(dateLeft: DayInput, dateRight: DayInput): boolean
|
|
348
|
+
/** @example isSameQuarter('2026-07-01', '2026-09-30') // true */
|
|
156
349
|
export function isSameQuarter(dateLeft: DayInput, dateRight: DayInput): boolean
|
|
157
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Comparator for `Array.prototype.sort`, oldest first.
|
|
353
|
+
* @example compareAsc('2026-01-01', '2026-08-08') // -1
|
|
354
|
+
*/
|
|
158
355
|
export function compareAsc(dateLeft: DayInput, dateRight: DayInput): -1 | 0 | 1
|
|
356
|
+
/** Newest first. @example compareDesc('2026-01-01', '2026-08-08') // 1 */
|
|
159
357
|
export function compareDesc(dateLeft: DayInput, dateRight: DayInput): -1 | 0 | 1
|
|
160
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Takes an **array**, not a rest argument.
|
|
361
|
+
* @example min(['2026-08-08', '2026-01-01']) // '2026-01-01'
|
|
362
|
+
*/
|
|
161
363
|
export function min(dates: DayInput[]): string
|
|
364
|
+
/**
|
|
365
|
+
* Takes an **array**, not a rest argument, exactly as `min` does.
|
|
366
|
+
* @example max(['2026-01-01', '2026-08-08']) // '2026-08-08'
|
|
367
|
+
*/
|
|
162
368
|
export function max(dates: DayInput[]): string
|
|
163
369
|
|
|
370
|
+
/** ISO weekday 7. @example isSunday('2026-08-09') // true */
|
|
164
371
|
export function isSunday(date: DayInput): boolean
|
|
372
|
+
/** ISO weekday 1. @example isMonday('2026-08-10') // true */
|
|
165
373
|
export function isMonday(date: DayInput): boolean
|
|
374
|
+
/** ISO weekday 2. @example isTuesday('2026-08-11') // true */
|
|
166
375
|
export function isTuesday(date: DayInput): boolean
|
|
376
|
+
/** ISO weekday 3. @example isWednesday('2026-08-12') // true */
|
|
167
377
|
export function isWednesday(date: DayInput): boolean
|
|
378
|
+
/** ISO weekday 4. @example isThursday('2026-08-13') // true */
|
|
168
379
|
export function isThursday(date: DayInput): boolean
|
|
380
|
+
/** ISO weekday 5. @example isFriday('2026-08-14') // true */
|
|
169
381
|
export function isFriday(date: DayInput): boolean
|
|
382
|
+
/** ISO weekday 6. @example isSaturday('2026-08-08') // true */
|
|
170
383
|
export function isSaturday(date: DayInput): boolean
|
|
384
|
+
/**
|
|
385
|
+
* ISO Saturday and Sunday (6 and 7). Not configurable, and no holiday calendar.
|
|
386
|
+
* @example isWeekend('2026-08-08') // true a Saturday
|
|
387
|
+
* @example isWeekend('2026-08-10') // false a Monday
|
|
388
|
+
*/
|
|
171
389
|
export function isWeekend(date: DayInput): boolean
|
|
390
|
+
/** @example isFirstDayOfMonth('2026-08-01') // true */
|
|
172
391
|
export function isFirstDayOfMonth(date: DayInput): boolean
|
|
392
|
+
/** @example isLastDayOfMonth('2024-02-29') // true leap year */
|
|
173
393
|
export function isLastDayOfMonth(date: DayInput): boolean
|
|
174
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Both ends **inclusive**, so a one-day interval returns one day.
|
|
397
|
+
* @example eachDayOfInterval({ start: '2026-08-08', end: '2026-08-10' })
|
|
398
|
+
* // ['2026-08-08', '2026-08-09', '2026-08-10']
|
|
399
|
+
*/
|
|
175
400
|
export function eachDayOfInterval(interval: Interval): string[]
|
|
401
|
+
/**
|
|
402
|
+
* The FIRST of each month touched, both ends inclusive.
|
|
403
|
+
* @example eachMonthOfInterval({ start: '2026-01-15', end: '2026-03-02' })
|
|
404
|
+
* // ['2026-01-01', '2026-02-01', '2026-03-01']
|
|
405
|
+
*/
|
|
176
406
|
export function eachMonthOfInterval(interval: Interval): string[]
|
|
407
|
+
/**
|
|
408
|
+
* The FIRST of January of each year touched, both ends inclusive.
|
|
409
|
+
* @example eachYearOfInterval({ start: '2025-06-01', end: '2026-02-01' })
|
|
410
|
+
* // ['2025-01-01', '2026-01-01']
|
|
411
|
+
*/
|
|
177
412
|
export function eachYearOfInterval(interval: Interval): string[]
|
|
413
|
+
/**
|
|
414
|
+
* Both ends **inclusive**, so a date equal to an endpoint is within.
|
|
415
|
+
* @example isWithinInterval('2026-08-10', { start: '2026-08-08', end: '2026-08-10' }) // true
|
|
416
|
+
*/
|
|
178
417
|
export function isWithinInterval(date: DayInput, interval: Interval): boolean
|
|
418
|
+
/**
|
|
419
|
+
* Pull a day inside the interval, or return it unchanged if it is already inside.
|
|
420
|
+
* @example clamp('2026-12-25', { start: '2026-08-08', end: '2026-08-10' }) // '2026-08-10'
|
|
421
|
+
*/
|
|
179
422
|
export function clamp(date: DayInput, interval: Interval): string
|
|
180
423
|
/**
|
|
181
424
|
* date-fns default: `inclusive: false` (touching endpoints only is not overlap).
|
|
182
425
|
* Pass `{ inclusive: true }` for closed intervals.
|
|
426
|
+
* @example areIntervalsOverlapping({ start: '2026-08-01', end: '2026-08-08' }, { start: '2026-08-08', end: '2026-08-10' }) // false
|
|
427
|
+
* @example areIntervalsOverlapping({ start: '2026-08-01', end: '2026-08-08' }, { start: '2026-08-08', end: '2026-08-10' }, { inclusive: true }) // true
|
|
183
428
|
*/
|
|
184
429
|
export function areIntervalsOverlapping(
|
|
185
430
|
intervalLeft: Interval,
|
package/index.js
CHANGED
|
@@ -55,6 +55,45 @@ const RUNTIME_CALENDARS = new Set(
|
|
|
55
55
|
*/
|
|
56
56
|
const ISO_DAY = /^(?:[+-]\d{6}|\d{4})-\d{2}-\d{2}$/u
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A day with a wall clock and no zone: `YYYY-MM-DD HH:MM[:SS[.fff]]`, with `T`,
|
|
60
|
+
* `t` or a space between them. Group 1 is the day. The clock gets dropped.
|
|
61
|
+
*
|
|
62
|
+
* This is what SQLite hands you — `datetime('now')`, `CURRENT_TIMESTAMP` and
|
|
63
|
+
* `strftime('%Y-%m-%d %H:%M:%f')` all emit a space, seconds and optional
|
|
64
|
+
* fractions — so a column value needs no reshaping before daymath reads it.
|
|
65
|
+
*
|
|
66
|
+
* **All three separators, because a separator is punctuation and never
|
|
67
|
+
* information.** Temporal accepts a space and a lowercase `t` wherever it accepts
|
|
68
|
+
* `T`, and the ZONED forms of all three already answered, so the only hole was a
|
|
69
|
+
* clock naming NO zone.
|
|
70
|
+
*
|
|
71
|
+
* **The hour is the only bound, because the hour is the only field that can
|
|
72
|
+
* change the date. That is the whole rule: daymath drops a clock only when the
|
|
73
|
+
* clock cannot move the day.** A leap second and a fraction of any length stay
|
|
74
|
+
* inside their own day, so both are accepted, matching what `day('…23:59:60Z')`
|
|
75
|
+
* already answered.
|
|
76
|
+
*
|
|
77
|
+
* **`24:00` is refused, and the reason is that SQLite disagrees with itself
|
|
78
|
+
* about it.** ISO `24:00` is midnight starting the NEXT day, and SQLite's own
|
|
79
|
+
* `julianday('2026-08-08 24:00:00')` is 2461261.5 — byte-identical to
|
|
80
|
+
* `julianday('2026-08-09 00:00:00')` — while its `date()` and
|
|
81
|
+
* `strftime('%Y-%m-%d')` on the same string both answer `'2026-08-08'`. So no
|
|
82
|
+
* answer daymath could give agrees with the database: the 9th contradicts its
|
|
83
|
+
* `date()`, the 8th contradicts its `julianday()`. Refusing is the only option
|
|
84
|
+
* that never silently disagrees. Temporal refuses `24:00` on every path too, so
|
|
85
|
+
* accepting would also mean doing carry arithmetic the engine will not do.
|
|
86
|
+
*
|
|
87
|
+
* That also separates it from the two cases above. `t` and `:60` were holes
|
|
88
|
+
* because the ZONED twin already answered; every `24:00` spelling is refused
|
|
89
|
+
* today, zoned and zoneless, so refusing it is the consistent choice.
|
|
90
|
+
*
|
|
91
|
+
* `Z`, an offset and a `[Zone]` cannot reach this pattern — it is anchored — so
|
|
92
|
+
* a string that names a real instant still takes the instant path in `day()`.
|
|
93
|
+
*/
|
|
94
|
+
const ISO_DAY_TIME =
|
|
95
|
+
/^((?:[+-]\d{6}|\d{4})-\d{2}-\d{2})[Tt ](?:[01]\d|2[0-3]):[0-5]\d(?::(?:[0-5]\d|60)(?:\.\d+)?)?$/u
|
|
96
|
+
|
|
58
97
|
/**
|
|
59
98
|
* Temporal's calendar annotation, `[u-ca=…]` or the critical `[!u-ca=…]`.
|
|
60
99
|
* Returns what it is attached to, and the calendar it names, or `null`.
|
|
@@ -153,13 +192,19 @@ const ZONE_LIKE =
|
|
|
153
192
|
* One predicate, because `toPlainDate` and `day()` both ask this question. They
|
|
154
193
|
* asked it separately once, and day() alone then refused a string that every
|
|
155
194
|
* other export accepted.
|
|
195
|
+
*
|
|
196
|
+
* `clock` reports whether a zoneless wall clock came off the string. Every export ignores it and
|
|
197
|
+
* answers the same day either way; only `day()` reads it, because `day()` is the only one that
|
|
198
|
+
* takes a zone and so the only one that can be asked to convert what it just discarded.
|
|
156
199
|
* @param {string} text
|
|
157
|
-
* @returns {string | null}
|
|
200
|
+
* @returns {{ day: string, clock: boolean } | null}
|
|
158
201
|
*/
|
|
159
202
|
function bareDay(text) {
|
|
160
203
|
const annotated = calendarAnnotation(text)
|
|
161
204
|
const bare = annotated ? annotated.head : text
|
|
162
|
-
|
|
205
|
+
if (ISO_DAY.test(bare)) return { day: bare, clock: false }
|
|
206
|
+
const clocked = ISO_DAY_TIME.exec(bare)
|
|
207
|
+
return clocked === null ? null : { day: clocked[1], clock: true }
|
|
163
208
|
}
|
|
164
209
|
|
|
165
210
|
/**
|
|
@@ -341,14 +386,15 @@ function toPlainDate(value, label = 'date') {
|
|
|
341
386
|
const bare = bareDay(text)
|
|
342
387
|
if (bare === null) {
|
|
343
388
|
throw new RangeError(
|
|
344
|
-
`daymath: ${label} must be ISO 8601 day YYYY-MM-DD or ±YYYYYY-MM-DD (got ${JSON.stringify(text)})`,
|
|
389
|
+
`daymath: ${label} must be ISO 8601 day YYYY-MM-DD or ±YYYYYY-MM-DD, optionally with a zoneless clock HH:MM[:SS[.fff]] after a T, t or space, where only the hour is bounded (00-23) (got ${JSON.stringify(text)})`,
|
|
345
390
|
)
|
|
346
391
|
}
|
|
347
392
|
try {
|
|
348
393
|
// The bare day is parsed with the ISO resolver's own entry point. `getAny` is passed as the
|
|
349
394
|
// resolver rather than called, because `fromString` calls it with whatever the string names,
|
|
350
|
-
// and a bare day names nothing.
|
|
351
|
-
|
|
395
|
+
// and a bare day names nothing. A wall clock is already off the string by this line, so no
|
|
396
|
+
// export ever parses one and every answer stays a day.
|
|
397
|
+
const plain = PlainDateFns.fromString(bare.day, getAny)
|
|
352
398
|
// The annotation rides along, so getYear answers 2569 for a Buddhist day and every
|
|
353
399
|
// returned string keeps the calendar the caller named.
|
|
354
400
|
return calendar === undefined
|
|
@@ -502,6 +548,9 @@ function toInterval(interval) {
|
|
|
502
548
|
* truncated the same way, so a fractional value is not an error
|
|
503
549
|
* - an ISO 8601 day string, or a `Temporal.PlainDate` from any implementation,
|
|
504
550
|
* both of which are already a day
|
|
551
|
+
* - a day carrying a zoneless wall clock, `'YYYY-MM-DD HH:MM[:SS[.fff]]'`, with
|
|
552
|
+
* `T`, `t` or a space between them: a day with noise on it, and the clock is
|
|
553
|
+
* dropped. Only the hour is bounded, at 00-23
|
|
505
554
|
* - an ISO 8601 timestamp carrying `Z` or an offset, which names an exact
|
|
506
555
|
* instant, so there is nothing left to guess
|
|
507
556
|
* - a string carrying a `[Zone]` annotation, which names its own zone, so it
|
|
@@ -518,9 +567,15 @@ function toInterval(interval) {
|
|
|
518
567
|
* ways in one function.
|
|
519
568
|
*
|
|
520
569
|
* `'11/12/2026'` is refused. Nobody can tell November from December in it.
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
570
|
+
*
|
|
571
|
+
* A **zoneless wall clock is a day**, so `'2026-08-08 12:00:00'` and
|
|
572
|
+
* `'2026-08-08T12:00'` both answer `'2026-08-08'`. The clock is dropped, never
|
|
573
|
+
* read. That is what SQLite hands you from `datetime()` and `CURRENT_TIMESTAMP`,
|
|
574
|
+
* and it takes the day path here exactly as a bare day does.
|
|
575
|
+
*
|
|
576
|
+
* **Pass `tz` with one and it throws.** Naming a zone means convert, and a clock
|
|
577
|
+
* with no zone gives nothing to convert from. Name the zone in the string —
|
|
578
|
+
* `'2026-08-08T12:00[America/New_York]'`, or `Z`, or an offset — and it converts.
|
|
524
579
|
*
|
|
525
580
|
* A lone string takes one of four roles, decided in this order: a day, then a
|
|
526
581
|
* zoned time, then an instant, then a zone. The zone test is by **shape**, and
|
|
@@ -543,20 +598,22 @@ function toInterval(interval) {
|
|
|
543
598
|
* annotation is accepted on input and dropped from the result. Every other export carries it.
|
|
544
599
|
* @throws {TypeError} If `moment` is not one of the accepted shapes
|
|
545
600
|
* @throws {RangeError} On an Invalid Date, a non-finite number, or an unknown zone
|
|
546
|
-
* @example day() //
|
|
547
|
-
* @example day('Asia/Tokyo') //
|
|
548
|
-
* @example day(row.createdAt) //
|
|
549
|
-
* @example day(row.createdAt, 'America/New_York') // '
|
|
601
|
+
* @example day() // today, in UTC
|
|
602
|
+
* @example day('Asia/Tokyo') // today in Tokyo
|
|
603
|
+
* @example day(row.createdAt) // a Date, read in UTC
|
|
604
|
+
* @example day(row.createdAt, 'America/New_York') // the same instant, New York's day
|
|
550
605
|
* @example day(1761616161771) // '2025-10-28' epoch ms
|
|
551
606
|
* @example day('1999-01-01T00:00:00Z') // '1999-01-01' an ISO timestamp
|
|
552
607
|
* @example day(zdt.toString()) // the zone in the string wins
|
|
553
|
-
* @example
|
|
608
|
+
* @example day(row.created_at) // a SQLite DATETIME
|
|
609
|
+
* @example day('2026-08-08 12:00:00', 'utc') // throws TypeError, the clock names no zone
|
|
610
|
+
* @example addDays(day(), 2) // two days from today
|
|
554
611
|
*/
|
|
555
612
|
export function day(moment, tz) {
|
|
556
613
|
// Already a day, in every accepted spelling. `bareDay` is the same predicate
|
|
557
614
|
// toPlainDate uses, so day() cannot drift from the other 68 exports.
|
|
558
|
-
const
|
|
559
|
-
|
|
615
|
+
const bare = typeof moment === 'string' ? bareDay(moment) : null
|
|
616
|
+
const isDay = isPlainDate(moment) || bare !== null
|
|
560
617
|
const isMoment = isDay || moment instanceof Date || typeof moment === 'number'
|
|
561
618
|
|
|
562
619
|
let zone = tz
|
|
@@ -672,7 +729,25 @@ export function day(moment, tz) {
|
|
|
672
729
|
// plain ISO day comes out. Carrying it here also could not be made
|
|
673
730
|
// consistent, because an `Instant` has no fields for a calendar to renumber,
|
|
674
731
|
// so that path has nothing to carry and would answer bare ISO regardless.
|
|
675
|
-
if (isDay)
|
|
732
|
+
if (isDay) {
|
|
733
|
+
// The value is judged FIRST, so a day that does not exist reports itself rather than the zone.
|
|
734
|
+
// The guard below is about the argument PAIR, so it must never pre-empt a fault in either half.
|
|
735
|
+
const answer = toDayString(isoOf(toPlainDate(moment)))
|
|
736
|
+
// The one argument pair daymath refuses rather than answers. On 2026-08-24 SQLite's
|
|
737
|
+
// `datetime('now')` read 2026-08-24 01:57:31 and `datetime('now','localtime')` read
|
|
738
|
+
// 2026-08-23 21:57:31 — different days — because `datetime()` defaults to UTC. So the caller
|
|
739
|
+
// who stores the default and asks for a local day is the one a silent answer would mislead.
|
|
740
|
+
//
|
|
741
|
+
// Only this pair. A bare day plus a zone still answers, because no information was discarded
|
|
742
|
+
// there. Sibling of the `two time zones` refusals above, which a wall clock never reaches:
|
|
743
|
+
// both of those sit inside `if (!isMoment …)`.
|
|
744
|
+
if (bare?.clock && tz !== undefined && tz !== null) {
|
|
745
|
+
throw new TypeError(
|
|
746
|
+
`daymath: day() cannot apply the zone ${JSON.stringify(tz)} to ${JSON.stringify(moment)}, because that clock names no zone (add Z or an offset to it, or drop the zone)`,
|
|
747
|
+
)
|
|
748
|
+
}
|
|
749
|
+
return answer
|
|
750
|
+
}
|
|
676
751
|
|
|
677
752
|
// The string named its own zone, so that zone decides the day, not the
|
|
678
753
|
// default. This is the one path where `zone` is deliberately not consulted.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "daymath",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Calendar date math (ISO 8601 day / PlainDate). date-fns-shaped. No Date. No time zones.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
"test:runtimes": "node scripts/cross-runtime.mjs",
|
|
30
30
|
"test:runtimes:write": "node scripts/cross-runtime.mjs --write",
|
|
31
31
|
"test:intl-day": "node scripts/intl-day.mjs",
|
|
32
|
+
"test:examples": "node scripts/examples.mjs",
|
|
33
|
+
"test:examples:write": "node scripts/examples.mjs --write",
|
|
32
34
|
"test:intl-day:sweep": "node scripts/intl-day.mjs --sweep",
|
|
33
35
|
"size": "node scripts/bundle-size.mjs --run",
|
|
34
36
|
"size:check": "node scripts/bundle-size.mjs --check",
|
|
35
37
|
"size:write": "node scripts/bundle-size.mjs --write",
|
|
36
|
-
"prepublishOnly": "npm run test:coverage",
|
|
38
|
+
"prepublishOnly": "npm run test:coverage && npm run test:examples",
|
|
37
39
|
"publish:github": "node scripts/publish-github-packages.mjs"
|
|
38
40
|
},
|
|
39
41
|
"keywords": [
|
|
@@ -67,9 +69,9 @@
|
|
|
67
69
|
"devDependencies": {
|
|
68
70
|
"c8": "^12.0.0",
|
|
69
71
|
"date-fns": "4.4.0",
|
|
70
|
-
"esbuild": "0.28.
|
|
71
|
-
"oxfmt": "0.
|
|
72
|
-
"oxlint": "1.
|
|
72
|
+
"esbuild": "0.28.2",
|
|
73
|
+
"oxfmt": "0.64.0",
|
|
74
|
+
"oxlint": "1.78.0",
|
|
73
75
|
"typescript": "7.0.2"
|
|
74
76
|
}
|
|
75
77
|
}
|