Skip to content

Commit 89bea6f

Browse files
feat: localDate/localTime/semver2 min/max to allow undefined inputs
1 parent 2471bce commit 89bea6f

File tree

5 files changed

+72
-33
lines changed

5 files changed

+72
-33
lines changed

src/datetime/localDate.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ test('sort', () => {
101101

102102
expect(localDate.min(items).toString()).toBe('2022-01-01')
103103
expect(localDate.max(items).toString()).toBe('2022-01-03')
104+
105+
// Test that undefined values are allowed
106+
expect(localDate.max([...items, undefined]).toString()).toBe('2022-01-03')
104107
})
105108

106109
test('add basic', () => {

src/datetime/localDate.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { _assert } from '../error/assert'
2+
import { _isTruthy } from '../is.util'
23
import { Iterable2 } from '../iter/iterable2'
34
import type {
45
Inclusiveness,
@@ -18,6 +19,7 @@ const MDAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
1819
const DATE_REGEX = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
1920

2021
export type LocalDateInput = LocalDate | Date | IsoDateString
22+
export type LocalDateInputNullable = LocalDateInput | null | undefined
2123
export type LocalDateFormatter = (ld: LocalDate) => string
2224

2325
/**
@@ -517,7 +519,7 @@ class LocalDateFactory {
517519
* Tries to construct LocalDate from LocalDateInput, returns null otherwise.
518520
* Does not throw (returns null instead).
519521
*/
520-
parseOrNull(d: LocalDateInput | undefined | null): LocalDate | null {
522+
parseOrNull(d: LocalDateInputNullable): LocalDate | null {
521523
if (!d) return null
522524
if (d instanceof LocalDate) return d
523525
if (d instanceof Date) {
@@ -619,35 +621,39 @@ class LocalDateFactory {
619621
/**
620622
* Returns the earliest (min) LocalDate from the array, or undefined if the array is empty.
621623
*/
622-
minOrUndefined(items: LocalDateInput[]): LocalDate | undefined {
624+
minOrUndefined(items: LocalDateInputNullable[]): LocalDate | undefined {
623625
return items.length ? this.min(items) : undefined
624626
}
625627

626628
/**
627629
* Returns the earliest LocalDate from the array.
628630
* Throws if the array is empty.
629631
*/
630-
min(items: LocalDateInput[]): LocalDate {
631-
_assert(items.length, 'localDate.min called on empty array')
632+
min(items: LocalDateInputNullable[]): LocalDate {
633+
const items2 = items.filter(_isTruthy)
634+
_assert(items2.length, 'localDate.min called on empty array')
632635

633-
return items.map(i => this.of(i)).reduce((min, item) => (min.isSameOrBefore(item) ? min : item))
636+
return items2
637+
.map(i => this.of(i))
638+
.reduce((min, item) => (min.isSameOrBefore(item) ? min : item))
634639
}
635640

636641
/**
637642
* Returns the latest (max) LocalDate from the array, or undefined if the array is empty.
638643
*/
639-
maxOrUndefined(items: LocalDateInput[]): LocalDate | undefined {
644+
maxOrUndefined(items: LocalDateInputNullable[]): LocalDate | undefined {
640645
return items.length ? this.max(items) : undefined
641646
}
642647

643648
/**
644649
* Returns the latest LocalDate from the array.
645650
* Throws if the array is empty.
646651
*/
647-
max(items: LocalDateInput[]): LocalDate {
648-
_assert(items.length, 'localDate.max called on empty array')
652+
max(items: LocalDateInputNullable[]): LocalDate {
653+
const items2 = items.filter(_isTruthy)
654+
_assert(items2.length, 'localDate.max called on empty array')
649655

650-
return items.map(i => this.of(i)).reduce((max, item) => (max.isSameOrAfter(item) ? max : item))
656+
return items2.map(i => this.of(i)).reduce((max, item) => (max.isSameOrAfter(item) ? max : item))
651657
}
652658

653659
/**
@@ -711,14 +717,14 @@ class LocalDateFactory {
711717
*
712718
* Similar to `localDate.orToday`, but that will instead return Today on falsy input.
713719
*/
714-
orUndefined(d: LocalDateInput | null | undefined): LocalDate | undefined {
720+
orUndefined(d: LocalDateInputNullable): LocalDate | undefined {
715721
return d ? this.of(d) : undefined
716722
}
717723

718724
/**
719725
* Creates a LocalDate from the input, unless it's falsy - then returns localDate.today.
720726
*/
721-
orToday(d: LocalDateInput | null | undefined): LocalDate {
727+
orToday(d: LocalDateInputNullable): LocalDate {
722728
return d ? this.of(d) : this.today()
723729
}
724730
}

src/datetime/localTime.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { _assert } from '../error/assert'
2+
import { _isTruthy } from '../is.util'
23
import { _ms } from '../time/time.util'
34
import type {
45
Inclusiveness,
@@ -27,6 +28,7 @@ export enum ISODayOfWeek {
2728
}
2829

2930
export type LocalTimeInput = LocalTime | Date | IsoDateTimeString | UnixTimestampNumber
31+
export type LocalTimeInputNullable = LocalTimeInput | null | undefined
3032
export type LocalTimeFormatter = (ld: LocalTime) => string
3133

3234
export type DateTimeObject = DateObject & TimeObject
@@ -711,7 +713,7 @@ class LocalTimeFactory {
711713
/**
712714
* Returns null if invalid
713715
*/
714-
parseOrNull(d: LocalTimeInput | undefined | null): LocalTime | null {
716+
parseOrNull(d: LocalTimeInputNullable): LocalTime | null {
715717
if (!d) return null
716718
if (d instanceof LocalTime) return d
717719

@@ -768,7 +770,7 @@ class LocalTimeFactory {
768770
return date.valueOf() / 1000
769771
}
770772

771-
isValid(d: LocalTimeInput | undefined | null): boolean {
773+
isValid(d: LocalTimeInputNullable): boolean {
772774
return this.parseOrNull(d) !== null
773775
}
774776

@@ -800,14 +802,14 @@ class LocalTimeFactory {
800802
*
801803
* `localTime` function will instead return LocalTime of `now` for falsy input.
802804
*/
803-
orUndefined(d: LocalTimeInput | null | undefined): LocalTime | undefined {
805+
orUndefined(d: LocalTimeInputNullable): LocalTime | undefined {
804806
return d ? this.of(d) : undefined
805807
}
806808

807809
/**
808810
* Creates a LocalTime from the input, unless it's falsy - then returns LocalTime.now
809811
*/
810-
orNow(d: LocalTimeInput | null | undefined): LocalTime {
812+
orNow(d: LocalTimeInputNullable): LocalTime {
811813
return d ? this.of(d) : this.now()
812814
}
813815

@@ -827,26 +829,28 @@ class LocalTimeFactory {
827829
})
828830
}
829831

830-
minOrUndefined(items: LocalTimeInput[]): LocalTime | undefined {
832+
minOrUndefined(items: LocalTimeInputNullable[]): LocalTime | undefined {
831833
return items.length ? this.min(items) : undefined
832834
}
833835

834-
min(items: LocalTimeInput[]): LocalTime {
835-
_assert(items.length, 'localTime.min called on empty array')
836+
min(items: LocalTimeInputNullable[]): LocalTime {
837+
const items2 = items.filter(_isTruthy)
838+
_assert(items2.length, 'localTime.min called on empty array')
836839

837-
return items
840+
return items2
838841
.map(i => this.of(i))
839842
.reduce((min, item) => (min.$date.valueOf() <= item.$date.valueOf() ? min : item))
840843
}
841844

842-
maxOrUndefined(items: LocalTimeInput[]): LocalTime | undefined {
845+
maxOrUndefined(items: LocalTimeInputNullable[]): LocalTime | undefined {
843846
return items.length ? this.max(items) : undefined
844847
}
845848

846-
max(items: LocalTimeInput[]): LocalTime {
847-
_assert(items.length, 'localTime.max called on empty array')
849+
max(items: LocalTimeInputNullable[]): LocalTime {
850+
const items2 = items.filter(_isTruthy)
851+
_assert(items2.length, 'localTime.max called on empty array')
848852

849-
return items
853+
return items2
850854
.map(i => this.of(i))
851855
.reduce((max, item) => (max.$date.valueOf() >= item.$date.valueOf() ? max : item))
852856
}

src/semver.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ test('basic', () => {
2727
)
2828
})
2929

30-
test('min, max', () => {
30+
test('min, max, sort', () => {
3131
expect(semver2.min(['1.2.3', '1.2.4']).toString()).toBe('1.2.3')
32+
expect(semver2.min(['1.2.3', undefined]).toString()).toBe('1.2.3')
3233
expect(semver2.minOrUndefined(['1.2.3', '1.2.4'])?.toString()).toBe('1.2.3')
3334
expect(semver2.minOrUndefined(['1.2.5'])?.toString()).toBe('1.2.5')
3435
expect(semver2.minOrUndefined([])).toBeUndefined()
@@ -37,6 +38,16 @@ test('min, max', () => {
3738
expect(semver2.maxOrUndefined(['1.2.3', '1.2.4'])?.toString()).toBe('1.2.4')
3839
expect(semver2.maxOrUndefined(['1.2.5'])?.toString()).toBe('1.2.5')
3940
expect(semver2.maxOrUndefined([])).toBeUndefined()
41+
42+
expect(semver2.sort(['1.2.4', '1.2.5', '1.1.9', '1.7.7'].map(semver2)).map(s => s.toString()))
43+
.toMatchInlineSnapshot(`
44+
[
45+
"1.1.9",
46+
"1.2.4",
47+
"1.2.5",
48+
"1.7.7",
49+
]
50+
`)
4051
})
4152

4253
test.each([

src/semver.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { _range } from './array/range'
22
import { _assert } from './error/assert'
3+
import { _isTruthy } from './is.util'
4+
import { SortDirection } from './types'
35

46
export type SemverInput = string | Semver
7+
export type SemverInputNullable = SemverInput | null | undefined
58
export type SemverTokens = [major: number, minor: number, patch: number]
69

710
/**
@@ -72,7 +75,7 @@ class SemverFactory {
7275
return s
7376
}
7477

75-
parseOrNull(input: SemverInput | undefined | null): Semver | null {
78+
parseOrNull(input: SemverInputNullable): Semver | null {
7679
if (!input) return null
7780
if (input instanceof Semver) return input
7881

@@ -83,33 +86,45 @@ class SemverFactory {
8386
/**
8487
* Returns the highest (max) Semver from the array, or undefined if the array is empty.
8588
*/
86-
maxOrUndefined(items: SemverInput[]): Semver | undefined {
89+
maxOrUndefined(items: SemverInputNullable[]): Semver | undefined {
8790
return items.length ? this.max(items) : undefined
8891
}
8992

9093
/**
9194
* Returns the highest Semver from the array.
9295
* Throws if the array is empty.
9396
*/
94-
max(items: SemverInput[]): Semver {
95-
_assert(items.length, 'semver.max called on empty array')
96-
return items.map(i => this.of(i)).reduce((max, item) => (max.isSameOrAfter(item) ? max : item))
97+
max(items: SemverInputNullable[]): Semver {
98+
const items2 = items.filter(_isTruthy)
99+
_assert(items2.length, 'semver.max called on empty array')
100+
return items2.map(i => this.of(i)).reduce((max, item) => (max.isSameOrAfter(item) ? max : item))
97101
}
98102

99103
/**
100104
* Returns the lowest (min) Semver from the array, or undefined if the array is empty.
101105
*/
102-
minOrUndefined(items: SemverInput[]): Semver | undefined {
106+
minOrUndefined(items: SemverInputNullable[]): Semver | undefined {
103107
return items.length ? this.min(items) : undefined
104108
}
105109

106110
/**
107111
* Returns the lowest Semver from the array.
108112
* Throws if the array is empty.
109113
*/
110-
min(items: SemverInput[]): Semver {
111-
_assert(items.length, 'semver.min called on empty array')
112-
return items.map(i => this.of(i)).reduce((min, item) => (min.isSameOrBefore(item) ? min : item))
114+
min(items: SemverInputNullable[]): Semver {
115+
const items2 = items.filter(_isTruthy)
116+
_assert(items2.length, 'semver.min called on empty array')
117+
return items2
118+
.map(i => this.of(i))
119+
.reduce((min, item) => (min.isSameOrBefore(item) ? min : item))
120+
}
121+
122+
/**
123+
* Sorts an array of Semvers in `dir` order (ascending by default).
124+
*/
125+
sort(items: Semver[], dir: SortDirection = 'asc', mutate = false): Semver[] {
126+
const mod = dir === 'desc' ? -1 : 1
127+
return (mutate ? items : [...items]).sort((a, b) => a.cmp(b) * mod)
113128
}
114129
}
115130

0 commit comments

Comments
 (0)