@naturalcycles/js-lib 14.232.0 → 14.233.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/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 +46 -51
- package/dist/datetime/localTime.js +181 -197
- 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 +177 -192
- package/dist-esm/datetime/timeInterval.js +5 -5
- package/dist-esm/env/buildInfo.js +2 -2
- package/package.json +2 -2
- package/src/datetime/dateInterval.ts +6 -7
- package/src/datetime/localDate.ts +307 -237
- package/src/datetime/localTime.ts +203 -212
- package/src/datetime/timeInterval.ts +6 -7
- package/src/env/buildInfo.ts +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { localDate } from './localDate';
|
|
2
2
|
/**
|
|
3
3
|
* Class that supports ISO8601 "Time interval" standard that looks like `2022-02-24/2022-03-30`.
|
|
4
4
|
*
|
|
@@ -10,7 +10,7 @@ export class DateInterval {
|
|
|
10
10
|
this.end = end;
|
|
11
11
|
}
|
|
12
12
|
static of(start, end) {
|
|
13
|
-
return new DateInterval(
|
|
13
|
+
return new DateInterval(localDate(start), localDate(end));
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Parses string like `2022-02-24/2023-03-30` into a DateInterval.
|
|
@@ -22,7 +22,7 @@ export class DateInterval {
|
|
|
22
22
|
if (!end || !start) {
|
|
23
23
|
throw new Error(`Cannot parse "${d}" into DateInterval`);
|
|
24
24
|
}
|
|
25
|
-
return new DateInterval(
|
|
25
|
+
return new DateInterval(localDate(start), localDate(end));
|
|
26
26
|
}
|
|
27
27
|
isSame(d) {
|
|
28
28
|
return this.cmp(d) === 0;
|
|
@@ -45,7 +45,7 @@ export class DateInterval {
|
|
|
45
45
|
* Ranges of DateInterval (start, end) are INCLUSIVE.
|
|
46
46
|
*/
|
|
47
47
|
includes(d, incl = '[]') {
|
|
48
|
-
d =
|
|
48
|
+
d = localDate(d);
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
|
|
50
50
|
return d.isAfter(this.start, incl[0] === '[') && d.isBefore(this.end, incl[1] === ']');
|
|
51
51
|
}
|
|
@@ -63,13 +63,13 @@ export class DateInterval {
|
|
|
63
63
|
return this.start.cmp(d.start) || this.end.cmp(d.end);
|
|
64
64
|
}
|
|
65
65
|
getDays(incl = '[]') {
|
|
66
|
-
return
|
|
66
|
+
return localDate.range(this.start, this.end, incl, 1, 'day');
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* Returns an array of LocalDates that are included in the interval.
|
|
70
70
|
*/
|
|
71
71
|
range(incl = '[]', step = 1, stepUnit = 'day') {
|
|
72
|
-
return
|
|
72
|
+
return localDate.range(this.start, this.end, incl, step, stepUnit);
|
|
73
73
|
}
|
|
74
74
|
toString() {
|
|
75
75
|
return [this.start, this.end].join('/');
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { _assert } from '../error/assert';
|
|
2
2
|
import { Iterable2 } from '../iter/iterable2';
|
|
3
|
-
import {
|
|
3
|
+
import { localTime } from './localTime';
|
|
4
4
|
const MDAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
5
5
|
const DATE_REGEX = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* LocalDate represents a date without time.
|
|
8
|
+
* It is timezone-independent.
|
|
8
9
|
*/
|
|
9
10
|
export class LocalDate {
|
|
10
11
|
constructor($year, $month, $day) {
|
|
@@ -12,90 +13,6 @@ export class LocalDate {
|
|
|
12
13
|
this.$month = $month;
|
|
13
14
|
this.$day = $day;
|
|
14
15
|
}
|
|
15
|
-
static create(year, month, day) {
|
|
16
|
-
return new LocalDate(year, month, day);
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Parses input into LocalDate.
|
|
20
|
-
* Input can already be a LocalDate - it is returned as-is in that case.
|
|
21
|
-
*/
|
|
22
|
-
static of(d) {
|
|
23
|
-
const t = this.parseOrNull(d);
|
|
24
|
-
_assert(t !== null, `Cannot parse "${d}" into LocalDate`, {
|
|
25
|
-
input: d,
|
|
26
|
-
});
|
|
27
|
-
return t;
|
|
28
|
-
}
|
|
29
|
-
static parseCompact(d) {
|
|
30
|
-
const [year, month, day] = [d.slice(0, 4), d.slice(4, 2), d.slice(6, 2)].map(Number);
|
|
31
|
-
_assert(day && month && year, `Cannot parse "${d}" into LocalDate`);
|
|
32
|
-
return new LocalDate(year, month, day);
|
|
33
|
-
}
|
|
34
|
-
static fromDate(d) {
|
|
35
|
-
return new LocalDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
|
|
36
|
-
}
|
|
37
|
-
static fromDateUTC(d) {
|
|
38
|
-
return new LocalDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Returns null if invalid.
|
|
42
|
-
*/
|
|
43
|
-
static parseOrNull(d) {
|
|
44
|
-
if (!d)
|
|
45
|
-
return null;
|
|
46
|
-
if (d instanceof LocalDate)
|
|
47
|
-
return d;
|
|
48
|
-
if (d instanceof Date) {
|
|
49
|
-
return this.fromDate(d);
|
|
50
|
-
}
|
|
51
|
-
const matches = typeof d === 'string' && DATE_REGEX.exec(d.slice(0, 10));
|
|
52
|
-
if (!matches)
|
|
53
|
-
return null;
|
|
54
|
-
const year = Number(matches[1]);
|
|
55
|
-
const month = Number(matches[2]);
|
|
56
|
-
const day = Number(matches[3]);
|
|
57
|
-
if (!year ||
|
|
58
|
-
!month ||
|
|
59
|
-
month < 1 ||
|
|
60
|
-
month > 12 ||
|
|
61
|
-
!day ||
|
|
62
|
-
day < 1 ||
|
|
63
|
-
day > this.getMonthLength(year, month)) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
return new LocalDate(year, month, day);
|
|
67
|
-
}
|
|
68
|
-
static isValid(iso) {
|
|
69
|
-
return this.parseOrNull(iso) !== null;
|
|
70
|
-
}
|
|
71
|
-
static today() {
|
|
72
|
-
return this.fromDate(new Date());
|
|
73
|
-
}
|
|
74
|
-
static todayUTC() {
|
|
75
|
-
return this.fromDateUTC(new Date());
|
|
76
|
-
}
|
|
77
|
-
static sort(items, mutate = false, dir = 'asc') {
|
|
78
|
-
const mod = dir === 'desc' ? -1 : 1;
|
|
79
|
-
return (mutate ? items : [...items]).sort((a, b) => a.cmp(b) * mod);
|
|
80
|
-
}
|
|
81
|
-
static earliestOrUndefined(items) {
|
|
82
|
-
return items.length ? LocalDate.earliest(items) : undefined;
|
|
83
|
-
}
|
|
84
|
-
static earliest(items) {
|
|
85
|
-
_assert(items.length, 'LocalDate.earliest called on empty array');
|
|
86
|
-
return items
|
|
87
|
-
.map(i => LocalDate.of(i))
|
|
88
|
-
.reduce((min, item) => (min.isSameOrBefore(item) ? min : item));
|
|
89
|
-
}
|
|
90
|
-
static latestOrUndefined(items) {
|
|
91
|
-
return items.length ? LocalDate.latest(items) : undefined;
|
|
92
|
-
}
|
|
93
|
-
static latest(items) {
|
|
94
|
-
_assert(items.length, 'LocalDate.latest called on empty array');
|
|
95
|
-
return items
|
|
96
|
-
.map(i => LocalDate.of(i))
|
|
97
|
-
.reduce((max, item) => (max.isSameOrAfter(item) ? max : item));
|
|
98
|
-
}
|
|
99
16
|
get(unit) {
|
|
100
17
|
return unit === 'year' ? this.$year : unit === 'month' ? this.$month : this.$day;
|
|
101
18
|
}
|
|
@@ -125,7 +42,7 @@ export class LocalDate {
|
|
|
125
42
|
return (this.toDate().getDay() || 7);
|
|
126
43
|
}
|
|
127
44
|
isSame(d) {
|
|
128
|
-
d =
|
|
45
|
+
d = localDate.of(d);
|
|
129
46
|
return this.$day === d.$day && this.$month === d.$month && this.$year === d.$year;
|
|
130
47
|
}
|
|
131
48
|
isBefore(d, inclusive = false) {
|
|
@@ -162,13 +79,13 @@ export class LocalDate {
|
|
|
162
79
|
* Third argument allows to override "today".
|
|
163
80
|
*/
|
|
164
81
|
isOlderThan(n, unit, today) {
|
|
165
|
-
return this.isBefore(
|
|
82
|
+
return this.isBefore(localDate.of(today || new Date()).plus(-n, unit));
|
|
166
83
|
}
|
|
167
84
|
/**
|
|
168
85
|
* Checks if this localDate is same or older (<=) than "today" by X units.
|
|
169
86
|
*/
|
|
170
87
|
isSameOrOlderThan(n, unit, today) {
|
|
171
|
-
return this.isSameOrBefore(
|
|
88
|
+
return this.isSameOrBefore(localDate.of(today || new Date()).plus(-n, unit));
|
|
172
89
|
}
|
|
173
90
|
/**
|
|
174
91
|
* Checks if this localDate is younger (>) than "today" by X units.
|
|
@@ -180,13 +97,13 @@ export class LocalDate {
|
|
|
180
97
|
* Third argument allows to override "today".
|
|
181
98
|
*/
|
|
182
99
|
isYoungerThan(n, unit, today) {
|
|
183
|
-
return this.isAfter(
|
|
100
|
+
return this.isAfter(localDate.of(today || new Date()).plus(-n, unit));
|
|
184
101
|
}
|
|
185
102
|
/**
|
|
186
103
|
* Checks if this localDate is same or younger (>=) than "today" by X units.
|
|
187
104
|
*/
|
|
188
105
|
isSameOrYoungerThan(n, unit, today) {
|
|
189
|
-
return this.isSameOrAfter(
|
|
106
|
+
return this.isSameOrAfter(localDate.of(today || new Date()).plus(-n, unit));
|
|
190
107
|
}
|
|
191
108
|
/**
|
|
192
109
|
* Returns 1 if this > d
|
|
@@ -194,7 +111,7 @@ export class LocalDate {
|
|
|
194
111
|
* returns -1 if this < d
|
|
195
112
|
*/
|
|
196
113
|
cmp(d) {
|
|
197
|
-
d =
|
|
114
|
+
d = localDate.of(d);
|
|
198
115
|
if (this.$year < d.$year)
|
|
199
116
|
return -1;
|
|
200
117
|
if (this.$year > d.$year)
|
|
@@ -221,7 +138,7 @@ export class LocalDate {
|
|
|
221
138
|
* a.diff(b) means "a minus b"
|
|
222
139
|
*/
|
|
223
140
|
diff(d, unit) {
|
|
224
|
-
d =
|
|
141
|
+
d = localDate.of(d);
|
|
225
142
|
const sign = this.cmp(d);
|
|
226
143
|
if (!sign)
|
|
227
144
|
return 0;
|
|
@@ -232,8 +149,8 @@ export class LocalDate {
|
|
|
232
149
|
if (big.$month < small.$month ||
|
|
233
150
|
(big.$month === small.$month &&
|
|
234
151
|
big.$day < small.$day &&
|
|
235
|
-
!(big.$day ===
|
|
236
|
-
small.$day ===
|
|
152
|
+
!(big.$day === localDate.getMonthLength(big.$year, big.$month) &&
|
|
153
|
+
small.$day === localDate.getMonthLength(small.$year, small.$month)))) {
|
|
237
154
|
years--;
|
|
238
155
|
}
|
|
239
156
|
return years * sign || 0;
|
|
@@ -241,7 +158,7 @@ export class LocalDate {
|
|
|
241
158
|
if (unit === 'month') {
|
|
242
159
|
let months = (big.$year - small.$year) * 12 + (big.$month - small.$month);
|
|
243
160
|
if (big.$day < small.$day) {
|
|
244
|
-
const bigMonthLen =
|
|
161
|
+
const bigMonthLen = localDate.getMonthLength(big.$year, big.$month);
|
|
245
162
|
if (big.$day !== bigMonthLen || small.$day < bigMonthLen) {
|
|
246
163
|
months--;
|
|
247
164
|
}
|
|
@@ -253,16 +170,16 @@ export class LocalDate {
|
|
|
253
170
|
// If small date is after 1st of March - next year's "leapness" should be used
|
|
254
171
|
const offsetYear = small.$month >= 3 ? 1 : 0;
|
|
255
172
|
for (let year = small.$year; year < big.$year; year++) {
|
|
256
|
-
days +=
|
|
173
|
+
days += localDate.getYearLength(year + offsetYear);
|
|
257
174
|
}
|
|
258
175
|
if (small.$month < big.$month) {
|
|
259
176
|
for (let month = small.$month; month < big.$month; month++) {
|
|
260
|
-
days +=
|
|
177
|
+
days += localDate.getMonthLength(big.$year, month);
|
|
261
178
|
}
|
|
262
179
|
}
|
|
263
180
|
else if (big.$month < small.$month) {
|
|
264
181
|
for (let month = big.$month; month < small.$month; month++) {
|
|
265
|
-
days -=
|
|
182
|
+
days -= localDate.getMonthLength(big.$year, month);
|
|
266
183
|
}
|
|
267
184
|
}
|
|
268
185
|
if (unit === 'week') {
|
|
@@ -303,11 +220,11 @@ export class LocalDate {
|
|
|
303
220
|
$year -= 1;
|
|
304
221
|
$month += 12;
|
|
305
222
|
}
|
|
306
|
-
$day +=
|
|
223
|
+
$day += localDate.getMonthLength($year, $month);
|
|
307
224
|
}
|
|
308
225
|
}
|
|
309
226
|
else {
|
|
310
|
-
let monLen =
|
|
227
|
+
let monLen = localDate.getMonthLength($year, $month);
|
|
311
228
|
if (unit !== 'day') {
|
|
312
229
|
if ($day > monLen) {
|
|
313
230
|
// Case of 2022-05-31 plus 1 month should be 2022-06-30, not 31
|
|
@@ -322,7 +239,7 @@ export class LocalDate {
|
|
|
322
239
|
$year += 1;
|
|
323
240
|
$month -= 12;
|
|
324
241
|
}
|
|
325
|
-
monLen =
|
|
242
|
+
monLen = localDate.getMonthLength($year, $month);
|
|
326
243
|
}
|
|
327
244
|
}
|
|
328
245
|
}
|
|
@@ -341,35 +258,24 @@ export class LocalDate {
|
|
|
341
258
|
if (unit === 'day')
|
|
342
259
|
return this;
|
|
343
260
|
if (unit === 'month')
|
|
344
|
-
return LocalDate
|
|
261
|
+
return new LocalDate(this.$year, this.$month, 1);
|
|
345
262
|
// year
|
|
346
|
-
return LocalDate
|
|
263
|
+
return new LocalDate(this.$year, 1, 1);
|
|
347
264
|
}
|
|
348
265
|
endOf(unit) {
|
|
349
266
|
if (unit === 'day')
|
|
350
267
|
return this;
|
|
351
268
|
if (unit === 'month')
|
|
352
|
-
return LocalDate
|
|
269
|
+
return new LocalDate(this.$year, this.$month, localDate.getMonthLength(this.$year, this.$month));
|
|
353
270
|
// year
|
|
354
|
-
return LocalDate
|
|
271
|
+
return new LocalDate(this.$year, 12, 31);
|
|
355
272
|
}
|
|
356
273
|
/**
|
|
357
274
|
* Returns how many days are in the current month.
|
|
358
275
|
* E.g 31 for January.
|
|
359
276
|
*/
|
|
360
277
|
daysInMonth() {
|
|
361
|
-
return
|
|
362
|
-
}
|
|
363
|
-
static getYearLength(year) {
|
|
364
|
-
return this.isLeapYear(year) ? 366 : 365;
|
|
365
|
-
}
|
|
366
|
-
static getMonthLength(year, month) {
|
|
367
|
-
if (month === 2)
|
|
368
|
-
return this.isLeapYear(year) ? 29 : 28;
|
|
369
|
-
return MDAYS[month];
|
|
370
|
-
}
|
|
371
|
-
static isLeapYear(year) {
|
|
372
|
-
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
278
|
+
return localDate.getMonthLength(this.$year, this.$month);
|
|
373
279
|
}
|
|
374
280
|
clone() {
|
|
375
281
|
return new LocalDate(this.$year, this.$month, this.$day);
|
|
@@ -388,33 +294,45 @@ export class LocalDate {
|
|
|
388
294
|
* Unlike normal `.toDate` that uses browser's timezone by default.
|
|
389
295
|
*/
|
|
390
296
|
toDateInUTC() {
|
|
391
|
-
return new Date(this.
|
|
297
|
+
return new Date(this.toISODateTimeInUTC());
|
|
392
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Converts LocalDate to LocalTime with 0 hours, 0 minutes, 0 seconds.
|
|
301
|
+
* LocalTime's Date will be in local timezone.
|
|
302
|
+
*/
|
|
393
303
|
toLocalTime() {
|
|
394
|
-
return
|
|
304
|
+
return localTime.of(this.toDate());
|
|
395
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Returns e.g: `1984-06-21`
|
|
308
|
+
*/
|
|
396
309
|
toISODate() {
|
|
397
|
-
return
|
|
310
|
+
return [
|
|
311
|
+
String(this.$year).padStart(4, '0'),
|
|
312
|
+
String(this.$month).padStart(2, '0'),
|
|
313
|
+
String(this.$day).padStart(2, '0'),
|
|
314
|
+
].join('-');
|
|
398
315
|
}
|
|
399
316
|
/**
|
|
400
|
-
* Returns e.g: `1984-06-
|
|
317
|
+
* Returns e.g: `1984-06-21T00:00:00`
|
|
318
|
+
* Hours, minutes and seconds are 0.
|
|
401
319
|
*/
|
|
402
320
|
toISODateTime() {
|
|
403
|
-
return this.
|
|
321
|
+
return this.toISODate() + 'T00:00:00';
|
|
404
322
|
}
|
|
405
323
|
/**
|
|
406
|
-
* Returns e.g: `1984-06-
|
|
324
|
+
* Returns e.g: `1984-06-21T00:00:00Z` (notice the Z at the end, which indicates UTC).
|
|
325
|
+
* Hours, minutes and seconds are 0.
|
|
407
326
|
*/
|
|
408
|
-
|
|
327
|
+
toISODateTimeInUTC() {
|
|
409
328
|
return this.toISODateTime() + 'Z';
|
|
410
329
|
}
|
|
411
330
|
toString() {
|
|
412
|
-
return
|
|
413
|
-
String(this.$year).padStart(4, '0'),
|
|
414
|
-
String(this.$month).padStart(2, '0'),
|
|
415
|
-
String(this.$day).padStart(2, '0'),
|
|
416
|
-
].join('-');
|
|
331
|
+
return this.toISODate();
|
|
417
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Returns e.g: `19840621`
|
|
335
|
+
*/
|
|
418
336
|
toStringCompact() {
|
|
419
337
|
return [
|
|
420
338
|
String(this.$year).padStart(4, '0'),
|
|
@@ -422,18 +340,26 @@ export class LocalDate {
|
|
|
422
340
|
String(this.$day).padStart(2, '0'),
|
|
423
341
|
].join('');
|
|
424
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Returns e.g: `1984-06`
|
|
345
|
+
*/
|
|
425
346
|
toMonthId() {
|
|
426
|
-
return this.
|
|
347
|
+
return this.toISODate().slice(0, 7);
|
|
427
348
|
}
|
|
428
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Returns unix timestamp of 00:00:00 of that date (in UTC, because unix timestamp always reflects UTC).
|
|
351
|
+
*/
|
|
429
352
|
unix() {
|
|
430
353
|
return Math.floor(this.toDate().valueOf() / 1000);
|
|
431
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Same as .unix(), but in milliseconds.
|
|
357
|
+
*/
|
|
432
358
|
unixMillis() {
|
|
433
359
|
return this.toDate().valueOf();
|
|
434
360
|
}
|
|
435
361
|
toJSON() {
|
|
436
|
-
return this.
|
|
362
|
+
return this.toISODate();
|
|
437
363
|
}
|
|
438
364
|
format(fmt) {
|
|
439
365
|
if (fmt instanceof Intl.DateTimeFormat) {
|
|
@@ -442,71 +368,209 @@ export class LocalDate {
|
|
|
442
368
|
return fmt(this);
|
|
443
369
|
}
|
|
444
370
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
371
|
+
class LocalDateFactory {
|
|
372
|
+
/**
|
|
373
|
+
* Create LocalDate from year, month and day components.
|
|
374
|
+
*/
|
|
375
|
+
create(year, month, day) {
|
|
376
|
+
return new LocalDate(year, month, day);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Create LocalDate from LocalDateInput.
|
|
380
|
+
* Input can already be a LocalDate - it is returned as-is in that case.
|
|
381
|
+
* String - will be parsed as yyyy-mm-dd.
|
|
382
|
+
* Date - will be converted to LocalDate (as-is, in whatever timezone it is - local or UTC).
|
|
383
|
+
* No other formats are supported.
|
|
384
|
+
*
|
|
385
|
+
* Will throw if it fails to parse/construct LocalDate.
|
|
386
|
+
*/
|
|
387
|
+
of(d) {
|
|
388
|
+
const t = this.parseOrNull(d);
|
|
389
|
+
_assert(t !== null, `Cannot parse "${d}" into LocalDate`, {
|
|
390
|
+
input: d,
|
|
391
|
+
});
|
|
392
|
+
return t;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Tries to construct LocalDate from LocalDateInput, returns null otherwise.
|
|
396
|
+
* Does not throw (returns null instead).
|
|
397
|
+
*/
|
|
398
|
+
parseOrNull(d) {
|
|
399
|
+
if (!d)
|
|
400
|
+
return null;
|
|
401
|
+
if (d instanceof LocalDate)
|
|
402
|
+
return d;
|
|
403
|
+
if (d instanceof Date) {
|
|
404
|
+
return this.fromDate(d);
|
|
405
|
+
}
|
|
406
|
+
const matches = typeof d === 'string' && DATE_REGEX.exec(d.slice(0, 10));
|
|
407
|
+
if (!matches)
|
|
408
|
+
return null;
|
|
409
|
+
const year = Number(matches[1]);
|
|
410
|
+
const month = Number(matches[2]);
|
|
411
|
+
const day = Number(matches[3]);
|
|
412
|
+
if (!year ||
|
|
413
|
+
!month ||
|
|
414
|
+
month < 1 ||
|
|
415
|
+
month > 12 ||
|
|
416
|
+
!day ||
|
|
417
|
+
day < 1 ||
|
|
418
|
+
day > this.getMonthLength(year, month)) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
return new LocalDate(year, month, day);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Parses "compact iso8601 format", e.g `19840621` into LocalDate.
|
|
425
|
+
* Throws if it fails to do so.
|
|
426
|
+
*/
|
|
427
|
+
parseCompact(d) {
|
|
428
|
+
const [year, month, day] = [d.slice(0, 4), d.slice(4, 2), d.slice(6, 2)].map(Number);
|
|
429
|
+
_assert(day && month && year, `Cannot parse "${d}" into LocalDate`);
|
|
430
|
+
return new LocalDate(year, month, day);
|
|
431
|
+
}
|
|
432
|
+
getYearLength(year) {
|
|
433
|
+
return this.isLeapYear(year) ? 366 : 365;
|
|
434
|
+
}
|
|
435
|
+
getMonthLength(year, month) {
|
|
436
|
+
if (month === 2)
|
|
437
|
+
return this.isLeapYear(year) ? 29 : 28;
|
|
438
|
+
return MDAYS[month];
|
|
439
|
+
}
|
|
440
|
+
isLeapYear(year) {
|
|
441
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Constructs LocalDate from Date.
|
|
445
|
+
* Takes Date as-is, in its timezone - local or UTC.
|
|
446
|
+
*/
|
|
447
|
+
fromDate(d) {
|
|
448
|
+
return new LocalDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Constructs LocalDate from Date.
|
|
452
|
+
* Takes Date's year/month/day components in UTC, using getUTCFullYear, getUTCMonth, getUTCDate.
|
|
453
|
+
*/
|
|
454
|
+
fromDateInUTC(d) {
|
|
455
|
+
return new LocalDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Returns true if isoString is a valid iso8601 string like `yyyy-mm-dd`.
|
|
459
|
+
*/
|
|
460
|
+
isValid(isoString) {
|
|
461
|
+
return this.parseOrNull(isoString) !== null;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Creates LocalDate that represents `today` (in local timezone).
|
|
465
|
+
*/
|
|
466
|
+
today() {
|
|
467
|
+
return this.fromDate(new Date());
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Creates LocalDate that represents `today` in UTC.
|
|
471
|
+
*/
|
|
472
|
+
todayInUTC() {
|
|
473
|
+
return this.fromDateInUTC(new Date());
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Sorts an array of LocalDates in `dir` order (ascending by default).
|
|
477
|
+
*/
|
|
478
|
+
sort(items, dir = 'asc', mutate = false) {
|
|
479
|
+
const mod = dir === 'desc' ? -1 : 1;
|
|
480
|
+
return (mutate ? items : [...items]).sort((a, b) => a.cmp(b) * mod);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Returns the earliest (min) LocalDate from the array, or undefined if the array is empty.
|
|
484
|
+
*/
|
|
485
|
+
minOrUndefined(items) {
|
|
486
|
+
return items.length ? this.min(items) : undefined;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Returns the earliest LocalDate from the array.
|
|
490
|
+
* Throws if the array is empty.
|
|
491
|
+
*/
|
|
492
|
+
min(items) {
|
|
493
|
+
_assert(items.length, 'localDate.min called on empty array');
|
|
494
|
+
return items.map(i => this.of(i)).reduce((min, item) => (min.isSameOrBefore(item) ? min : item));
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Returns the latest (max) LocalDate from the array, or undefined if the array is empty.
|
|
498
|
+
*/
|
|
499
|
+
maxOrUndefined(items) {
|
|
500
|
+
return items.length ? this.max(items) : undefined;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Returns the latest LocalDate from the array.
|
|
504
|
+
* Throws if the array is empty.
|
|
505
|
+
*/
|
|
506
|
+
max(items) {
|
|
507
|
+
_assert(items.length, 'localDate.max called on empty array');
|
|
508
|
+
return items.map(i => this.of(i)).reduce((max, item) => (max.isSameOrAfter(item) ? max : item));
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Returns the range (array) of LocalDates between min and max.
|
|
512
|
+
* By default, min is included, max is excluded.
|
|
513
|
+
*/
|
|
514
|
+
range(min, max, incl = '[)', step = 1, stepUnit = 'day') {
|
|
515
|
+
return this.rangeIterable(min, max, incl, step, stepUnit).toArray();
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Returns the Iterable2 of LocalDates between min and max.
|
|
519
|
+
* By default, min is included, max is excluded.
|
|
520
|
+
*/
|
|
521
|
+
rangeIterable(min, max, incl = '[)', step = 1, stepUnit = 'day') {
|
|
522
|
+
if (stepUnit === 'week') {
|
|
523
|
+
step *= 7;
|
|
524
|
+
stepUnit = 'day';
|
|
525
|
+
}
|
|
526
|
+
const $min = this.of(min).startOf(stepUnit);
|
|
527
|
+
const $max = this.of(max).startOf(stepUnit);
|
|
528
|
+
let value = $min;
|
|
529
|
+
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
|
|
530
|
+
if (value.isAfter($min, incl[0] === '[')) {
|
|
531
|
+
// ok
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
value.plus(1, stepUnit, true);
|
|
535
|
+
}
|
|
536
|
+
const rightInclusive = incl[1] === ']';
|
|
537
|
+
return Iterable2.of({
|
|
538
|
+
*[Symbol.iterator]() {
|
|
539
|
+
while (value.isBefore($max, rightInclusive)) {
|
|
540
|
+
yield value;
|
|
541
|
+
// We don't mutate, because we already returned `current`
|
|
542
|
+
// in the previous iteration
|
|
543
|
+
value = value.plus(step, stepUnit);
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Creates a LocalDate from the input, unless it's falsy - then returns undefined.
|
|
550
|
+
*
|
|
551
|
+
* Similar to `localDate.orToday`, but that will instead return Today on falsy input.
|
|
552
|
+
*/
|
|
553
|
+
orUndefined(d) {
|
|
554
|
+
return d ? this.of(d) : undefined;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Creates a LocalDate from the input, unless it's falsy - then returns localDate.today.
|
|
558
|
+
*/
|
|
559
|
+
orToday(d) {
|
|
560
|
+
return d ? this.of(d) : this.today();
|
|
561
|
+
}
|
|
503
562
|
}
|
|
563
|
+
const localDateFactory = new LocalDateFactory();
|
|
564
|
+
// export const localDate = Object.assign((d: LocalDateInput) => {
|
|
565
|
+
// return localDateFactory.of(d)
|
|
566
|
+
// }, localDateFactory) as LocalDateFn
|
|
567
|
+
export const localDate = localDateFactory.of.bind(localDateFactory);
|
|
568
|
+
// The line below is the blackest of black magic I have ever written in 2024.
|
|
569
|
+
// And probably 2023 as well.
|
|
570
|
+
Object.setPrototypeOf(localDate, localDateFactory);
|
|
504
571
|
/**
|
|
505
572
|
Convenience function to return current today's IsoDateString representation, e.g `2024-06-21`
|
|
506
573
|
*/
|
|
507
574
|
export function todayString() {
|
|
508
|
-
|
|
509
|
-
// return new Date().toISOString().slice(0, 10)
|
|
510
|
-
// But, toISOString always returns the date in UTC, so in the Browser it would give unexpected result!
|
|
511
|
-
return LocalDate.fromDate(new Date()).toString();
|
|
575
|
+
return localDate.fromDate(new Date()).toISODate();
|
|
512
576
|
}
|