@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.
@@ -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
- * @experimental
24
+ * LocalDate represents a date without time.
25
+ * It is timezone-independent.
25
26
  */
26
27
  export class LocalDate {
27
- private constructor(
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 = LocalDate.of(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(LocalDate.of(today || new Date()).plus(-n, unit))
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(LocalDate.of(today || new Date()).plus(-n, unit))
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(LocalDate.of(today || new Date()).plus(-n, unit))
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(LocalDate.of(today || new Date()).plus(-n, unit))
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 = LocalDate.of(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 = LocalDate.of(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 === LocalDate.getMonthLength(big.$year, big.$month) &&
296
- small.$day === LocalDate.getMonthLength(small.$year, small.$month)
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 = LocalDate.getMonthLength(big.$year, big.$month)
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 += LocalDate.getYearLength(year + offsetYear)
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 += LocalDate.getMonthLength(big.$year, month)
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 -= LocalDate.getMonthLength(big.$year, month)
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 += LocalDate.getMonthLength($year, $month)
272
+ $day += localDate.getMonthLength($year, $month)
379
273
  }
380
274
  } else {
381
- let monLen = LocalDate.getMonthLength($year, $month)
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 = LocalDate.getMonthLength($year, $month)
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.create(this.$year, this.$month, 1)
312
+ if (unit === 'month') return new LocalDate(this.$year, this.$month, 1)
419
313
  // year
420
- return LocalDate.create(this.$year, 1, 1)
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.create(
320
+ return new LocalDate(
427
321
  this.$year,
428
322
  this.$month,
429
- LocalDate.getMonthLength(this.$year, this.$month),
323
+ localDate.getMonthLength(this.$year, this.$month),
430
324
  )
431
325
  // year
432
- return LocalDate.create(this.$year, 12, 31)
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 LocalDate.getMonthLength(this.$year, this.$month)
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.toISODateTimeUTC())
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 LocalTime.of(this.toDate())
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 this.toString()
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-21T17:56:21`
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.toString() + 'T00:00:00'
383
+ return this.toISODate() + 'T00:00:00'
491
384
  }
492
385
 
493
386
  /**
494
- * Returns e.g: `1984-06-21T17:56:21Z` (notice the Z at the end, which indicates UTC)
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
- toISODateTimeUTC(): IsoDateTimeString {
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.toString().slice(0, 7)
413
+ return this.toISODate().slice(0, 7)
518
414
  }
519
415
 
520
- // May be not optimal, as LocalTime better suits it
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.toString()
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
- export function localDateRange(
543
- min: LocalDateInput,
544
- max: LocalDateInput,
545
- incl: Inclusiveness = '[)',
546
- step = 1,
547
- stepUnit: LocalDateUnit = 'day',
548
- ): LocalDate[] {
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
- * Experimental, returns the range as Iterable2.
554
- */
555
- export function localDateRangeIterable(
556
- min: LocalDateInput,
557
- max: LocalDateInput,
558
- incl: Inclusiveness = '[)',
559
- step = 1,
560
- stepUnit: LocalDateUnit = 'day',
561
- ): Iterable2<LocalDate> {
562
- if (stepUnit === 'week') {
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
- * Convenience wrapper around `LocalDate.of`
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
- * Convenience wrapper around `LocalDate.today`
602
- */
603
- export function localDateToday(): LocalDate {
604
- return LocalDate.today()
605
- }
467
+ return t
468
+ }
606
469
 
607
- /**
608
- * Creates a LocalDate from the input, unless it's falsy - then returns undefined.
609
- *
610
- * `localDate` function will instead return LocalDate of today for falsy input.
611
- */
612
- export function localDateOrUndefined(d?: LocalDateInput | null): LocalDate | undefined {
613
- return d ? LocalDate.of(d) : undefined
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 | undefined): 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 | undefined): LocalDate {
676
+ return d ? this.of(d) : this.today()
677
+ }
614
678
  }
615
679
 
616
- /**
617
- * Creates a LocalDate from the input, unless it's falsy - then returns LocalDate.today.
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
- // It was benchmarked to be faster than by concatenating individual Date components
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
  }