@naturalcycles/js-lib 14.231.0 → 14.233.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/dist/datetime/dateInterval.d.ts +1 -2
- package/dist/datetime/dateInterval.js +5 -5
- package/dist/datetime/localDate.d.ts +125 -52
- package/dist/datetime/localDate.js +259 -201
- package/dist/datetime/localTime.d.ts +50 -46
- package/dist/datetime/localTime.js +187 -195
- package/dist/datetime/timeInterval.d.ts +1 -2
- package/dist/datetime/timeInterval.js +4 -4
- package/dist/env/buildInfo.js +1 -1
- package/dist-esm/datetime/dateInterval.js +6 -6
- package/dist-esm/datetime/localDate.js +259 -195
- package/dist-esm/datetime/localTime.js +183 -190
- package/dist-esm/datetime/timeInterval.js +5 -5
- package/dist-esm/env/buildInfo.js +2 -2
- package/package.json +4 -4
- package/src/datetime/dateInterval.ts +6 -7
- package/src/datetime/localDate.ts +307 -237
- package/src/datetime/localTime.ts +209 -210
- package/src/datetime/timeInterval.ts +6 -7
- package/src/env/buildInfo.ts +2 -2
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
UnixTimestampMillisNumber,
|
|
10
10
|
UnixTimestampNumber,
|
|
11
11
|
} from '../types'
|
|
12
|
-
import { ISODayOfWeek, LocalTime } from './localTime'
|
|
12
|
+
import { ISODayOfWeek, localTime, LocalTime } from './localTime'
|
|
13
13
|
|
|
14
14
|
export type LocalDateUnit = LocalDateUnitStrict | 'week'
|
|
15
15
|
export type LocalDateUnitStrict = 'year' | 'month' | 'day'
|
|
@@ -21,122 +21,16 @@ export type LocalDateInput = LocalDate | Date | IsoDateString
|
|
|
21
21
|
export type LocalDateFormatter = (ld: LocalDate) => string
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* LocalDate represents a date without time.
|
|
25
|
+
* It is timezone-independent.
|
|
25
26
|
*/
|
|
26
27
|
export class LocalDate {
|
|
27
|
-
|
|
28
|
+
constructor(
|
|
28
29
|
private $year: number,
|
|
29
30
|
private $month: number,
|
|
30
31
|
private $day: number,
|
|
31
32
|
) {}
|
|
32
33
|
|
|
33
|
-
static create(year: number, month: number, day: number): LocalDate {
|
|
34
|
-
return new LocalDate(year, month, day)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Parses input into LocalDate.
|
|
39
|
-
* Input can already be a LocalDate - it is returned as-is in that case.
|
|
40
|
-
*/
|
|
41
|
-
static of(d: LocalDateInput): LocalDate {
|
|
42
|
-
const t = this.parseOrNull(d)
|
|
43
|
-
|
|
44
|
-
_assert(t !== null, `Cannot parse "${d}" into LocalDate`, {
|
|
45
|
-
input: d,
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
return t
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
static parseCompact(d: string): LocalDate {
|
|
52
|
-
const [year, month, day] = [d.slice(0, 4), d.slice(4, 2), d.slice(6, 2)].map(Number)
|
|
53
|
-
|
|
54
|
-
_assert(day && month && year, `Cannot parse "${d}" into LocalDate`)
|
|
55
|
-
|
|
56
|
-
return new LocalDate(year, month, day)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
static fromDate(d: Date): LocalDate {
|
|
60
|
-
return new LocalDate(d.getFullYear(), d.getMonth() + 1, d.getDate())
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
static fromDateUTC(d: Date): LocalDate {
|
|
64
|
-
return new LocalDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate())
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Returns null if invalid.
|
|
69
|
-
*/
|
|
70
|
-
static parseOrNull(d: LocalDateInput | undefined | null): LocalDate | null {
|
|
71
|
-
if (!d) return null
|
|
72
|
-
if (d instanceof LocalDate) return d
|
|
73
|
-
if (d instanceof Date) {
|
|
74
|
-
return this.fromDate(d)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const matches = typeof (d as any) === 'string' && DATE_REGEX.exec(d.slice(0, 10))
|
|
78
|
-
if (!matches) return null
|
|
79
|
-
|
|
80
|
-
const year = Number(matches[1])
|
|
81
|
-
const month = Number(matches[2])
|
|
82
|
-
const day = Number(matches[3])
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
!year ||
|
|
86
|
-
!month ||
|
|
87
|
-
month < 1 ||
|
|
88
|
-
month > 12 ||
|
|
89
|
-
!day ||
|
|
90
|
-
day < 1 ||
|
|
91
|
-
day > this.getMonthLength(year, month)
|
|
92
|
-
) {
|
|
93
|
-
return null
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return new LocalDate(year, month, day)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
static isValid(iso: string | undefined | null): boolean {
|
|
100
|
-
return this.parseOrNull(iso) !== null
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
static today(): LocalDate {
|
|
104
|
-
return this.fromDate(new Date())
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
static todayUTC(): LocalDate {
|
|
108
|
-
return this.fromDateUTC(new Date())
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
static sort(items: LocalDate[], mutate = false, dir: SortDirection = 'asc'): LocalDate[] {
|
|
112
|
-
const mod = dir === 'desc' ? -1 : 1
|
|
113
|
-
return (mutate ? items : [...items]).sort((a, b) => a.cmp(b) * mod)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
static earliestOrUndefined(items: LocalDateInput[]): LocalDate | undefined {
|
|
117
|
-
return items.length ? LocalDate.earliest(items) : undefined
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
static earliest(items: LocalDateInput[]): LocalDate {
|
|
121
|
-
_assert(items.length, 'LocalDate.earliest called on empty array')
|
|
122
|
-
|
|
123
|
-
return items
|
|
124
|
-
.map(i => LocalDate.of(i))
|
|
125
|
-
.reduce((min, item) => (min.isSameOrBefore(item) ? min : item))
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
static latestOrUndefined(items: LocalDateInput[]): LocalDate | undefined {
|
|
129
|
-
return items.length ? LocalDate.latest(items) : undefined
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
static latest(items: LocalDateInput[]): LocalDate {
|
|
133
|
-
_assert(items.length, 'LocalDate.latest called on empty array')
|
|
134
|
-
|
|
135
|
-
return items
|
|
136
|
-
.map(i => LocalDate.of(i))
|
|
137
|
-
.reduce((max, item) => (max.isSameOrAfter(item) ? max : item))
|
|
138
|
-
}
|
|
139
|
-
|
|
140
34
|
get(unit: LocalDateUnitStrict): number {
|
|
141
35
|
return unit === 'year' ? this.$year : unit === 'month' ? this.$month : this.$day
|
|
142
36
|
}
|
|
@@ -176,7 +70,7 @@ export class LocalDate {
|
|
|
176
70
|
}
|
|
177
71
|
|
|
178
72
|
isSame(d: LocalDateInput): boolean {
|
|
179
|
-
d =
|
|
73
|
+
d = localDate.of(d)
|
|
180
74
|
return this.$day === d.$day && this.$month === d.$month && this.$year === d.$year
|
|
181
75
|
}
|
|
182
76
|
|
|
@@ -217,14 +111,14 @@ export class LocalDate {
|
|
|
217
111
|
* Third argument allows to override "today".
|
|
218
112
|
*/
|
|
219
113
|
isOlderThan(n: number, unit: LocalDateUnit, today?: LocalDateInput): boolean {
|
|
220
|
-
return this.isBefore(
|
|
114
|
+
return this.isBefore(localDate.of(today || new Date()).plus(-n, unit))
|
|
221
115
|
}
|
|
222
116
|
|
|
223
117
|
/**
|
|
224
118
|
* Checks if this localDate is same or older (<=) than "today" by X units.
|
|
225
119
|
*/
|
|
226
120
|
isSameOrOlderThan(n: number, unit: LocalDateUnit, today?: LocalDateInput): boolean {
|
|
227
|
-
return this.isSameOrBefore(
|
|
121
|
+
return this.isSameOrBefore(localDate.of(today || new Date()).plus(-n, unit))
|
|
228
122
|
}
|
|
229
123
|
|
|
230
124
|
/**
|
|
@@ -237,14 +131,14 @@ export class LocalDate {
|
|
|
237
131
|
* Third argument allows to override "today".
|
|
238
132
|
*/
|
|
239
133
|
isYoungerThan(n: number, unit: LocalDateUnit, today?: LocalDateInput): boolean {
|
|
240
|
-
return this.isAfter(
|
|
134
|
+
return this.isAfter(localDate.of(today || new Date()).plus(-n, unit))
|
|
241
135
|
}
|
|
242
136
|
|
|
243
137
|
/**
|
|
244
138
|
* Checks if this localDate is same or younger (>=) than "today" by X units.
|
|
245
139
|
*/
|
|
246
140
|
isSameOrYoungerThan(n: number, unit: LocalDateUnit, today?: LocalDateInput): boolean {
|
|
247
|
-
return this.isSameOrAfter(
|
|
141
|
+
return this.isSameOrAfter(localDate.of(today || new Date()).plus(-n, unit))
|
|
248
142
|
}
|
|
249
143
|
|
|
250
144
|
/**
|
|
@@ -253,7 +147,7 @@ export class LocalDate {
|
|
|
253
147
|
* returns -1 if this < d
|
|
254
148
|
*/
|
|
255
149
|
cmp(d: LocalDateInput): -1 | 0 | 1 {
|
|
256
|
-
d =
|
|
150
|
+
d = localDate.of(d)
|
|
257
151
|
if (this.$year < d.$year) return -1
|
|
258
152
|
if (this.$year > d.$year) return 1
|
|
259
153
|
if (this.$month < d.$month) return -1
|
|
@@ -276,7 +170,7 @@ export class LocalDate {
|
|
|
276
170
|
* a.diff(b) means "a minus b"
|
|
277
171
|
*/
|
|
278
172
|
diff(d: LocalDateInput, unit: LocalDateUnit): number {
|
|
279
|
-
d =
|
|
173
|
+
d = localDate.of(d)
|
|
280
174
|
|
|
281
175
|
const sign = this.cmp(d)
|
|
282
176
|
if (!sign) return 0
|
|
@@ -292,8 +186,8 @@ export class LocalDate {
|
|
|
292
186
|
(big.$month === small.$month &&
|
|
293
187
|
big.$day < small.$day &&
|
|
294
188
|
!(
|
|
295
|
-
big.$day ===
|
|
296
|
-
small.$day ===
|
|
189
|
+
big.$day === localDate.getMonthLength(big.$year, big.$month) &&
|
|
190
|
+
small.$day === localDate.getMonthLength(small.$year, small.$month)
|
|
297
191
|
))
|
|
298
192
|
) {
|
|
299
193
|
years--
|
|
@@ -305,7 +199,7 @@ export class LocalDate {
|
|
|
305
199
|
if (unit === 'month') {
|
|
306
200
|
let months = (big.$year - small.$year) * 12 + (big.$month - small.$month)
|
|
307
201
|
if (big.$day < small.$day) {
|
|
308
|
-
const bigMonthLen =
|
|
202
|
+
const bigMonthLen = localDate.getMonthLength(big.$year, big.$month)
|
|
309
203
|
if (big.$day !== bigMonthLen || small.$day < bigMonthLen) {
|
|
310
204
|
months--
|
|
311
205
|
}
|
|
@@ -319,16 +213,16 @@ export class LocalDate {
|
|
|
319
213
|
// If small date is after 1st of March - next year's "leapness" should be used
|
|
320
214
|
const offsetYear = small.$month >= 3 ? 1 : 0
|
|
321
215
|
for (let year = small.$year; year < big.$year; year++) {
|
|
322
|
-
days +=
|
|
216
|
+
days += localDate.getYearLength(year + offsetYear)
|
|
323
217
|
}
|
|
324
218
|
|
|
325
219
|
if (small.$month < big.$month) {
|
|
326
220
|
for (let month = small.$month; month < big.$month; month++) {
|
|
327
|
-
days +=
|
|
221
|
+
days += localDate.getMonthLength(big.$year, month)
|
|
328
222
|
}
|
|
329
223
|
} else if (big.$month < small.$month) {
|
|
330
224
|
for (let month = big.$month; month < small.$month; month++) {
|
|
331
|
-
days -=
|
|
225
|
+
days -= localDate.getMonthLength(big.$year, month)
|
|
332
226
|
}
|
|
333
227
|
}
|
|
334
228
|
|
|
@@ -375,10 +269,10 @@ export class LocalDate {
|
|
|
375
269
|
$month += 12
|
|
376
270
|
}
|
|
377
271
|
|
|
378
|
-
$day +=
|
|
272
|
+
$day += localDate.getMonthLength($year, $month)
|
|
379
273
|
}
|
|
380
274
|
} else {
|
|
381
|
-
let monLen =
|
|
275
|
+
let monLen = localDate.getMonthLength($year, $month)
|
|
382
276
|
|
|
383
277
|
if (unit !== 'day') {
|
|
384
278
|
if ($day > monLen) {
|
|
@@ -394,7 +288,7 @@ export class LocalDate {
|
|
|
394
288
|
$month -= 12
|
|
395
289
|
}
|
|
396
290
|
|
|
397
|
-
monLen =
|
|
291
|
+
monLen = localDate.getMonthLength($year, $month)
|
|
398
292
|
}
|
|
399
293
|
}
|
|
400
294
|
}
|
|
@@ -415,21 +309,21 @@ export class LocalDate {
|
|
|
415
309
|
|
|
416
310
|
startOf(unit: LocalDateUnitStrict): LocalDate {
|
|
417
311
|
if (unit === 'day') return this
|
|
418
|
-
if (unit === 'month') return LocalDate
|
|
312
|
+
if (unit === 'month') return new LocalDate(this.$year, this.$month, 1)
|
|
419
313
|
// year
|
|
420
|
-
return LocalDate
|
|
314
|
+
return new LocalDate(this.$year, 1, 1)
|
|
421
315
|
}
|
|
422
316
|
|
|
423
317
|
endOf(unit: LocalDateUnitStrict): LocalDate {
|
|
424
318
|
if (unit === 'day') return this
|
|
425
319
|
if (unit === 'month')
|
|
426
|
-
return LocalDate
|
|
320
|
+
return new LocalDate(
|
|
427
321
|
this.$year,
|
|
428
322
|
this.$month,
|
|
429
|
-
|
|
323
|
+
localDate.getMonthLength(this.$year, this.$month),
|
|
430
324
|
)
|
|
431
325
|
// year
|
|
432
|
-
return LocalDate
|
|
326
|
+
return new LocalDate(this.$year, 12, 31)
|
|
433
327
|
}
|
|
434
328
|
|
|
435
329
|
/**
|
|
@@ -437,20 +331,7 @@ export class LocalDate {
|
|
|
437
331
|
* E.g 31 for January.
|
|
438
332
|
*/
|
|
439
333
|
daysInMonth(): number {
|
|
440
|
-
return
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
static getYearLength(year: number): number {
|
|
444
|
-
return this.isLeapYear(year) ? 366 : 365
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
static getMonthLength(year: number, month: number): number {
|
|
448
|
-
if (month === 2) return this.isLeapYear(year) ? 29 : 28
|
|
449
|
-
return MDAYS[month]!
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
static isLeapYear(year: number): boolean {
|
|
453
|
-
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
|
|
334
|
+
return localDate.getMonthLength(this.$year, this.$month)
|
|
454
335
|
}
|
|
455
336
|
|
|
456
337
|
clone(): LocalDate {
|
|
@@ -472,39 +353,51 @@ export class LocalDate {
|
|
|
472
353
|
* Unlike normal `.toDate` that uses browser's timezone by default.
|
|
473
354
|
*/
|
|
474
355
|
toDateInUTC(): Date {
|
|
475
|
-
return new Date(this.
|
|
356
|
+
return new Date(this.toISODateTimeInUTC())
|
|
476
357
|
}
|
|
477
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Converts LocalDate to LocalTime with 0 hours, 0 minutes, 0 seconds.
|
|
361
|
+
* LocalTime's Date will be in local timezone.
|
|
362
|
+
*/
|
|
478
363
|
toLocalTime(): LocalTime {
|
|
479
|
-
return
|
|
364
|
+
return localTime.of(this.toDate())
|
|
480
365
|
}
|
|
481
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Returns e.g: `1984-06-21`
|
|
369
|
+
*/
|
|
482
370
|
toISODate(): IsoDateString {
|
|
483
|
-
return
|
|
371
|
+
return [
|
|
372
|
+
String(this.$year).padStart(4, '0'),
|
|
373
|
+
String(this.$month).padStart(2, '0'),
|
|
374
|
+
String(this.$day).padStart(2, '0'),
|
|
375
|
+
].join('-')
|
|
484
376
|
}
|
|
485
377
|
|
|
486
378
|
/**
|
|
487
|
-
* Returns e.g: `1984-06-
|
|
379
|
+
* Returns e.g: `1984-06-21T00:00:00`
|
|
380
|
+
* Hours, minutes and seconds are 0.
|
|
488
381
|
*/
|
|
489
382
|
toISODateTime(): IsoDateTimeString {
|
|
490
|
-
return this.
|
|
383
|
+
return this.toISODate() + 'T00:00:00'
|
|
491
384
|
}
|
|
492
385
|
|
|
493
386
|
/**
|
|
494
|
-
* Returns e.g: `1984-06-
|
|
387
|
+
* Returns e.g: `1984-06-21T00:00:00Z` (notice the Z at the end, which indicates UTC).
|
|
388
|
+
* Hours, minutes and seconds are 0.
|
|
495
389
|
*/
|
|
496
|
-
|
|
390
|
+
toISODateTimeInUTC(): IsoDateTimeString {
|
|
497
391
|
return this.toISODateTime() + 'Z'
|
|
498
392
|
}
|
|
499
393
|
|
|
500
394
|
toString(): IsoDateString {
|
|
501
|
-
return
|
|
502
|
-
String(this.$year).padStart(4, '0'),
|
|
503
|
-
String(this.$month).padStart(2, '0'),
|
|
504
|
-
String(this.$day).padStart(2, '0'),
|
|
505
|
-
].join('-')
|
|
395
|
+
return this.toISODate()
|
|
506
396
|
}
|
|
507
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Returns e.g: `19840621`
|
|
400
|
+
*/
|
|
508
401
|
toStringCompact(): string {
|
|
509
402
|
return [
|
|
510
403
|
String(this.$year).padStart(4, '0'),
|
|
@@ -513,21 +406,29 @@ export class LocalDate {
|
|
|
513
406
|
].join('')
|
|
514
407
|
}
|
|
515
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Returns e.g: `1984-06`
|
|
411
|
+
*/
|
|
516
412
|
toMonthId(): MonthId {
|
|
517
|
-
return this.
|
|
413
|
+
return this.toISODate().slice(0, 7)
|
|
518
414
|
}
|
|
519
415
|
|
|
520
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Returns unix timestamp of 00:00:00 of that date (in UTC, because unix timestamp always reflects UTC).
|
|
418
|
+
*/
|
|
521
419
|
unix(): UnixTimestampNumber {
|
|
522
420
|
return Math.floor(this.toDate().valueOf() / 1000)
|
|
523
421
|
}
|
|
524
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Same as .unix(), but in milliseconds.
|
|
425
|
+
*/
|
|
525
426
|
unixMillis(): UnixTimestampMillisNumber {
|
|
526
427
|
return this.toDate().valueOf()
|
|
527
428
|
}
|
|
528
429
|
|
|
529
430
|
toJSON(): IsoDateString {
|
|
530
|
-
return this.
|
|
431
|
+
return this.toISODate()
|
|
531
432
|
}
|
|
532
433
|
|
|
533
434
|
format(fmt: Intl.DateTimeFormat | LocalDateFormatter): string {
|
|
@@ -539,93 +440,262 @@ export class LocalDate {
|
|
|
539
440
|
}
|
|
540
441
|
}
|
|
541
442
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return localDateRangeIterable(min, max, incl, step, stepUnit).toArray()
|
|
550
|
-
}
|
|
443
|
+
class LocalDateFactory {
|
|
444
|
+
/**
|
|
445
|
+
* Create LocalDate from year, month and day components.
|
|
446
|
+
*/
|
|
447
|
+
create(year: number, month: number, day: number): LocalDate {
|
|
448
|
+
return new LocalDate(year, month, day)
|
|
449
|
+
}
|
|
551
450
|
|
|
552
|
-
/**
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
):
|
|
562
|
-
|
|
563
|
-
step *= 7
|
|
564
|
-
stepUnit = 'day'
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const $min = LocalDate.of(min).startOf(stepUnit)
|
|
568
|
-
const $max = LocalDate.of(max).startOf(stepUnit)
|
|
569
|
-
|
|
570
|
-
let value = $min
|
|
571
|
-
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
|
|
572
|
-
if (value.isAfter($min, incl[0] === '[')) {
|
|
573
|
-
// ok
|
|
574
|
-
} else {
|
|
575
|
-
value.plus(1, stepUnit, true)
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const rightInclusive = incl[1] === ']'
|
|
579
|
-
|
|
580
|
-
return Iterable2.of({
|
|
581
|
-
*[Symbol.iterator]() {
|
|
582
|
-
while (value.isBefore($max, rightInclusive)) {
|
|
583
|
-
yield value
|
|
584
|
-
|
|
585
|
-
// We don't mutate, because we already returned `current`
|
|
586
|
-
// in the previous iteration
|
|
587
|
-
value = value.plus(step, stepUnit)
|
|
588
|
-
}
|
|
589
|
-
},
|
|
590
|
-
})
|
|
591
|
-
}
|
|
451
|
+
/**
|
|
452
|
+
* Create LocalDate from LocalDateInput.
|
|
453
|
+
* Input can already be a LocalDate - it is returned as-is in that case.
|
|
454
|
+
* String - will be parsed as yyyy-mm-dd.
|
|
455
|
+
* Date - will be converted to LocalDate (as-is, in whatever timezone it is - local or UTC).
|
|
456
|
+
* No other formats are supported.
|
|
457
|
+
*
|
|
458
|
+
* Will throw if it fails to parse/construct LocalDate.
|
|
459
|
+
*/
|
|
460
|
+
of(d: LocalDateInput): LocalDate {
|
|
461
|
+
const t = this.parseOrNull(d)
|
|
592
462
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
export function localDate(d: LocalDateInput): LocalDate {
|
|
597
|
-
return LocalDate.of(d)
|
|
598
|
-
}
|
|
463
|
+
_assert(t !== null, `Cannot parse "${d}" into LocalDate`, {
|
|
464
|
+
input: d,
|
|
465
|
+
})
|
|
599
466
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
*/
|
|
603
|
-
export function localDateToday(): LocalDate {
|
|
604
|
-
return LocalDate.today()
|
|
605
|
-
}
|
|
467
|
+
return t
|
|
468
|
+
}
|
|
606
469
|
|
|
607
|
-
/**
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
470
|
+
/**
|
|
471
|
+
* Tries to construct LocalDate from LocalDateInput, returns null otherwise.
|
|
472
|
+
* Does not throw (returns null instead).
|
|
473
|
+
*/
|
|
474
|
+
parseOrNull(d: LocalDateInput | undefined | null): LocalDate | null {
|
|
475
|
+
if (!d) return null
|
|
476
|
+
if (d instanceof LocalDate) return d
|
|
477
|
+
if (d instanceof Date) {
|
|
478
|
+
return this.fromDate(d)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const matches = typeof (d as any) === 'string' && DATE_REGEX.exec(d.slice(0, 10))
|
|
482
|
+
if (!matches) return null
|
|
483
|
+
|
|
484
|
+
const year = Number(matches[1])
|
|
485
|
+
const month = Number(matches[2])
|
|
486
|
+
const day = Number(matches[3])
|
|
487
|
+
|
|
488
|
+
if (
|
|
489
|
+
!year ||
|
|
490
|
+
!month ||
|
|
491
|
+
month < 1 ||
|
|
492
|
+
month > 12 ||
|
|
493
|
+
!day ||
|
|
494
|
+
day < 1 ||
|
|
495
|
+
day > this.getMonthLength(year, month)
|
|
496
|
+
) {
|
|
497
|
+
return null
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return new LocalDate(year, month, day)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Parses "compact iso8601 format", e.g `19840621` into LocalDate.
|
|
505
|
+
* Throws if it fails to do so.
|
|
506
|
+
*/
|
|
507
|
+
parseCompact(d: string): LocalDate {
|
|
508
|
+
const [year, month, day] = [d.slice(0, 4), d.slice(4, 2), d.slice(6, 2)].map(Number)
|
|
509
|
+
|
|
510
|
+
_assert(day && month && year, `Cannot parse "${d}" into LocalDate`)
|
|
511
|
+
|
|
512
|
+
return new LocalDate(year, month, day)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
getYearLength(year: number): number {
|
|
516
|
+
return this.isLeapYear(year) ? 366 : 365
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
getMonthLength(year: number, month: number): number {
|
|
520
|
+
if (month === 2) return this.isLeapYear(year) ? 29 : 28
|
|
521
|
+
return MDAYS[month]!
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
isLeapYear(year: number): boolean {
|
|
525
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Constructs LocalDate from Date.
|
|
530
|
+
* Takes Date as-is, in its timezone - local or UTC.
|
|
531
|
+
*/
|
|
532
|
+
fromDate(d: Date): LocalDate {
|
|
533
|
+
return new LocalDate(d.getFullYear(), d.getMonth() + 1, d.getDate())
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Constructs LocalDate from Date.
|
|
538
|
+
* Takes Date's year/month/day components in UTC, using getUTCFullYear, getUTCMonth, getUTCDate.
|
|
539
|
+
*/
|
|
540
|
+
fromDateInUTC(d: Date): LocalDate {
|
|
541
|
+
return new LocalDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate())
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Returns true if isoString is a valid iso8601 string like `yyyy-mm-dd`.
|
|
546
|
+
*/
|
|
547
|
+
isValid(isoString: string | undefined | null): boolean {
|
|
548
|
+
return this.parseOrNull(isoString) !== null
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Creates LocalDate that represents `today` (in local timezone).
|
|
553
|
+
*/
|
|
554
|
+
today(): LocalDate {
|
|
555
|
+
return this.fromDate(new Date())
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Creates LocalDate that represents `today` in UTC.
|
|
560
|
+
*/
|
|
561
|
+
todayInUTC(): LocalDate {
|
|
562
|
+
return this.fromDateInUTC(new Date())
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Sorts an array of LocalDates in `dir` order (ascending by default).
|
|
567
|
+
*/
|
|
568
|
+
sort(items: LocalDate[], dir: SortDirection = 'asc', mutate = false): LocalDate[] {
|
|
569
|
+
const mod = dir === 'desc' ? -1 : 1
|
|
570
|
+
return (mutate ? items : [...items]).sort((a, b) => a.cmp(b) * mod)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Returns the earliest (min) LocalDate from the array, or undefined if the array is empty.
|
|
575
|
+
*/
|
|
576
|
+
minOrUndefined(items: LocalDateInput[]): LocalDate | undefined {
|
|
577
|
+
return items.length ? this.min(items) : undefined
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Returns the earliest LocalDate from the array.
|
|
582
|
+
* Throws if the array is empty.
|
|
583
|
+
*/
|
|
584
|
+
min(items: LocalDateInput[]): LocalDate {
|
|
585
|
+
_assert(items.length, 'localDate.min called on empty array')
|
|
586
|
+
|
|
587
|
+
return items.map(i => this.of(i)).reduce((min, item) => (min.isSameOrBefore(item) ? min : item))
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Returns the latest (max) LocalDate from the array, or undefined if the array is empty.
|
|
592
|
+
*/
|
|
593
|
+
maxOrUndefined(items: LocalDateInput[]): LocalDate | undefined {
|
|
594
|
+
return items.length ? this.max(items) : undefined
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Returns the latest LocalDate from the array.
|
|
599
|
+
* Throws if the array is empty.
|
|
600
|
+
*/
|
|
601
|
+
max(items: LocalDateInput[]): LocalDate {
|
|
602
|
+
_assert(items.length, 'localDate.max called on empty array')
|
|
603
|
+
|
|
604
|
+
return items.map(i => this.of(i)).reduce((max, item) => (max.isSameOrAfter(item) ? max : item))
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Returns the range (array) of LocalDates between min and max.
|
|
609
|
+
* By default, min is included, max is excluded.
|
|
610
|
+
*/
|
|
611
|
+
range(
|
|
612
|
+
min: LocalDateInput,
|
|
613
|
+
max: LocalDateInput,
|
|
614
|
+
incl: Inclusiveness = '[)',
|
|
615
|
+
step = 1,
|
|
616
|
+
stepUnit: LocalDateUnit = 'day',
|
|
617
|
+
): LocalDate[] {
|
|
618
|
+
return this.rangeIterable(min, max, incl, step, stepUnit).toArray()
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Returns the Iterable2 of LocalDates between min and max.
|
|
623
|
+
* By default, min is included, max is excluded.
|
|
624
|
+
*/
|
|
625
|
+
rangeIterable(
|
|
626
|
+
min: LocalDateInput,
|
|
627
|
+
max: LocalDateInput,
|
|
628
|
+
incl: Inclusiveness = '[)',
|
|
629
|
+
step = 1,
|
|
630
|
+
stepUnit: LocalDateUnit = 'day',
|
|
631
|
+
): Iterable2<LocalDate> {
|
|
632
|
+
if (stepUnit === 'week') {
|
|
633
|
+
step *= 7
|
|
634
|
+
stepUnit = 'day'
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const $min = this.of(min).startOf(stepUnit)
|
|
638
|
+
const $max = this.of(max).startOf(stepUnit)
|
|
639
|
+
|
|
640
|
+
let value = $min
|
|
641
|
+
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
|
|
642
|
+
if (value.isAfter($min, incl[0] === '[')) {
|
|
643
|
+
// ok
|
|
644
|
+
} else {
|
|
645
|
+
value.plus(1, stepUnit, true)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const rightInclusive = incl[1] === ']'
|
|
649
|
+
|
|
650
|
+
return Iterable2.of({
|
|
651
|
+
*[Symbol.iterator]() {
|
|
652
|
+
while (value.isBefore($max, rightInclusive)) {
|
|
653
|
+
yield value
|
|
654
|
+
|
|
655
|
+
// We don't mutate, because we already returned `current`
|
|
656
|
+
// in the previous iteration
|
|
657
|
+
value = value.plus(step, stepUnit)
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Creates a LocalDate from the input, unless it's falsy - then returns undefined.
|
|
665
|
+
*
|
|
666
|
+
* Similar to `localDate.orToday`, but that will instead return Today on falsy input.
|
|
667
|
+
*/
|
|
668
|
+
orUndefined(d?: LocalDateInput | null): LocalDate | undefined {
|
|
669
|
+
return d ? this.of(d) : undefined
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Creates a LocalDate from the input, unless it's falsy - then returns localDate.today.
|
|
674
|
+
*/
|
|
675
|
+
orToday(d?: LocalDateInput | null): LocalDate {
|
|
676
|
+
return d ? this.of(d) : this.today()
|
|
677
|
+
}
|
|
614
678
|
}
|
|
615
679
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
*/
|
|
619
|
-
export function localDateOrToday(d?: LocalDateInput | null): LocalDate {
|
|
620
|
-
return d ? LocalDate.of(d) : LocalDate.today()
|
|
680
|
+
interface LocalDateFn extends LocalDateFactory {
|
|
681
|
+
(d: LocalDateInput): LocalDate
|
|
621
682
|
}
|
|
622
683
|
|
|
684
|
+
const localDateFactory = new LocalDateFactory()
|
|
685
|
+
|
|
686
|
+
// export const localDate = Object.assign((d: LocalDateInput) => {
|
|
687
|
+
// return localDateFactory.of(d)
|
|
688
|
+
// }, localDateFactory) as LocalDateFn
|
|
689
|
+
|
|
690
|
+
export const localDate = localDateFactory.of.bind(localDateFactory) as LocalDateFn
|
|
691
|
+
|
|
692
|
+
// The line below is the blackest of black magic I have ever written in 2024.
|
|
693
|
+
// And probably 2023 as well.
|
|
694
|
+
Object.setPrototypeOf(localDate, localDateFactory)
|
|
695
|
+
|
|
623
696
|
/**
|
|
624
697
|
Convenience function to return current today's IsoDateString representation, e.g `2024-06-21`
|
|
625
698
|
*/
|
|
626
699
|
export function todayString(): IsoDateString {
|
|
627
|
-
|
|
628
|
-
// return new Date().toISOString().slice(0, 10)
|
|
629
|
-
// But, toISOString always returns the date in UTC, so in the Browser it would give unexpected result!
|
|
630
|
-
return LocalDate.fromDate(new Date()).toString()
|
|
700
|
+
return localDate.fromDate(new Date()).toISODate()
|
|
631
701
|
}
|