Skip to content

Commit 2471bce

Browse files
feat: refactor _semver into semver2
Similar pattern as localDate/localTime.
1 parent 7794cf7 commit 2471bce

File tree

3 files changed

+70
-51
lines changed

3 files changed

+70
-51
lines changed

scripts/semverBench.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ yarn tsn semverBench
66

77
import { runBenchScript } from '@naturalcycles/bench-lib'
88
import semver from 'semver'
9-
import { _range, _semver } from '../src'
9+
import { _range, semver2 } from '../src'
1010

1111
const data = _range(10).map(n => `${n}.${(n * 7) % 10}.${(n * 9) % 7}`)
1212
const data2 = _range(data.length).map(n => `${n}.${Math.round((n * 7.5) % 10)}.${(n * 5) % 7}`)
@@ -18,7 +18,7 @@ runBenchScript({
1818
const _a: any[] = []
1919

2020
_range(data.length).forEach(i => {
21-
_a.push(_semver(data[i]!).cmp(data2[i]!))
21+
_a.push(semver2(data[i]!).cmp(data2[i]!))
2222
})
2323
},
2424
semver: () => {

src/semver.test.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { _expectedErrorString } from './error/try'
2-
import { _semver, _semverCompare, Semver } from './semver'
2+
import { _quickSemverCompare, semver2 } from './semver'
33

44
test('basic', () => {
5-
const s = Semver.of('1.2.3')
5+
const s = semver2('1.2.3')
66
expect(s.toString()).toBe('1.2.3')
77
expect(`${s}`).toBe('1.2.3')
88
expect(JSON.stringify(s)).toBe('"1.2.3"')
@@ -12,21 +12,33 @@ test('basic', () => {
1212
expect(s.minor).toBe(2)
1313
expect(s.patch).toBe(3)
1414

15-
const s2 = Semver.of('1.2.5')
15+
const s2 = semver2.of('1.2.5')
1616
expect(s.cmp(s2)).toBe(-1)
1717
expect(s.isAfter(s2)).toBe(false)
1818
expect(s.isSameOrAfter(s2)).toBe(false)
1919
expect(s.isBefore(s2)).toBe(true)
2020
expect(s.isSameOrBefore(s2)).toBe(true)
2121
expect(s.isSame(s2)).toBe(false)
22-
expect(_semver('1.5.4').isSame('1.5.4')).toBe(true)
23-
expect(_semver('1.5.4').isSame(_semver('1.5.4'))).toBe(true)
22+
expect(semver2('1.5.4').isSame('1.5.4')).toBe(true)
23+
expect(semver2('1.5.4').isSame(semver2('1.5.4'))).toBe(true)
2424

25-
expect(_expectedErrorString(() => Semver.of(''))).toMatchInlineSnapshot(
25+
expect(_expectedErrorString(() => semver2(''))).toMatchInlineSnapshot(
2626
`"AssertionError: Cannot parse "" into Semver"`,
2727
)
2828
})
2929

30+
test('min, max', () => {
31+
expect(semver2.min(['1.2.3', '1.2.4']).toString()).toBe('1.2.3')
32+
expect(semver2.minOrUndefined(['1.2.3', '1.2.4'])?.toString()).toBe('1.2.3')
33+
expect(semver2.minOrUndefined(['1.2.5'])?.toString()).toBe('1.2.5')
34+
expect(semver2.minOrUndefined([])).toBeUndefined()
35+
36+
expect(semver2.max(['1.2.3', '1.2.4']).toString()).toBe('1.2.4')
37+
expect(semver2.maxOrUndefined(['1.2.3', '1.2.4'])?.toString()).toBe('1.2.4')
38+
expect(semver2.maxOrUndefined(['1.2.5'])?.toString()).toBe('1.2.5')
39+
expect(semver2.maxOrUndefined([])).toBeUndefined()
40+
})
41+
3042
test.each([
3143
[undefined, undefined],
3244
[null, undefined],
@@ -40,7 +52,7 @@ test.each([
4052
['.', '0.0.0'],
4153
['x', '0.0.0'],
4254
])('parse', (str, expected) => {
43-
expect(Semver.parseOrNull(str)?.toString()).toBe(expected)
55+
expect(semver2.parseOrNull(str)?.toString()).toBe(expected)
4456
})
4557

4658
test.each([
@@ -55,6 +67,6 @@ test.each([
5567
['1.1.3', '1.1.51', -1],
5668
['1.1.3', '1.1.11', -1],
5769
['1.1.11', '1.1.3', 1],
58-
])('_semverCompare "%s" "%s" is %s', (a, b, expected) => {
59-
expect(_semverCompare(a, b)).toBe(expected)
70+
])('_quickSemverCompare "%s" "%s" is %s', (a, b, expected) => {
71+
expect(_quickSemverCompare(a, b)).toBe(expected)
6072
})

src/semver.ts

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export type SemverTokens = [major: number, minor: number, patch: number]
2121
* @experimental
2222
*/
2323
export class Semver {
24-
private constructor(public tokens: SemverTokens) {}
24+
constructor(public tokens: SemverTokens) {}
2525

2626
get major(): number {
2727
return this.tokens[0]
@@ -33,7 +33,35 @@ export class Semver {
3333
return this.tokens[2]
3434
}
3535

36-
static of(input: SemverInput): Semver {
36+
isAfter = (other: SemverInput): boolean => this.cmp(other) > 0
37+
isSameOrAfter = (other: SemverInput): boolean => this.cmp(other) >= 0
38+
isBefore = (other: SemverInput): boolean => this.cmp(other) < 0
39+
isSameOrBefore = (other: SemverInput): boolean => this.cmp(other) <= 0
40+
isSame = (other: SemverInput): boolean => this.cmp(other) === 0
41+
42+
/**
43+
* Returns 1 if this > other
44+
* returns 0 if they are equal
45+
* returns -1 if this < other
46+
*/
47+
cmp(other: SemverInput): -1 | 0 | 1 {
48+
const { tokens } = semver2.of(other)
49+
for (let i = 0; i < 3; i++) {
50+
if (this.tokens[i]! < tokens[i]!) return -1
51+
if (this.tokens[i]! > tokens[i]!) return 1
52+
}
53+
return 0
54+
}
55+
56+
toJSON = (): string => this.toString()
57+
58+
toString(): string {
59+
return this.tokens.join('.')
60+
}
61+
}
62+
63+
class SemverFactory {
64+
of(input: SemverInput): Semver {
3765
const s = this.parseOrNull(input)
3866

3967
_assert(s !== null, `Cannot parse "${input}" into Semver`, {
@@ -44,7 +72,7 @@ export class Semver {
4472
return s
4573
}
4674

47-
static parseOrNull(input: SemverInput | undefined | null): Semver | null {
75+
parseOrNull(input: SemverInput | undefined | null): Semver | null {
4876
if (!input) return null
4977
if (input instanceof Semver) return input
5078

@@ -55,68 +83,47 @@ export class Semver {
5583
/**
5684
* Returns the highest (max) Semver from the array, or undefined if the array is empty.
5785
*/
58-
static maxOrUndefined(items: SemverInput[]): Semver | undefined {
59-
return items.length ? Semver.max(items) : undefined
86+
maxOrUndefined(items: SemverInput[]): Semver | undefined {
87+
return items.length ? this.max(items) : undefined
6088
}
6189

6290
/**
6391
* Returns the highest Semver from the array.
6492
* Throws if the array is empty.
6593
*/
66-
static max(items: SemverInput[]): Semver {
94+
max(items: SemverInput[]): Semver {
6795
_assert(items.length, 'semver.max called on empty array')
6896
return items.map(i => this.of(i)).reduce((max, item) => (max.isSameOrAfter(item) ? max : item))
6997
}
7098

7199
/**
72100
* Returns the lowest (min) Semver from the array, or undefined if the array is empty.
73101
*/
74-
static minOrUndefined(items: SemverInput[]): Semver | undefined {
75-
return items.length ? Semver.min(items) : undefined
102+
minOrUndefined(items: SemverInput[]): Semver | undefined {
103+
return items.length ? this.min(items) : undefined
76104
}
77105

78106
/**
79107
* Returns the lowest Semver from the array.
80108
* Throws if the array is empty.
81109
*/
82-
static min(items: SemverInput[]): Semver {
110+
min(items: SemverInput[]): Semver {
83111
_assert(items.length, 'semver.min called on empty array')
84112
return items.map(i => this.of(i)).reduce((min, item) => (min.isSameOrBefore(item) ? min : item))
85113
}
114+
}
86115

87-
isAfter = (other: SemverInput): boolean => this.cmp(other) > 0
88-
isSameOrAfter = (other: SemverInput): boolean => this.cmp(other) >= 0
89-
isBefore = (other: SemverInput): boolean => this.cmp(other) < 0
90-
isSameOrBefore = (other: SemverInput): boolean => this.cmp(other) <= 0
91-
isSame = (other: SemverInput): boolean => this.cmp(other) === 0
92-
93-
/**
94-
* Returns 1 if this > other
95-
* returns 0 if they are equal
96-
* returns -1 if this < other
97-
*/
98-
cmp(other: SemverInput): -1 | 0 | 1 {
99-
const { tokens } = Semver.of(other)
100-
for (let i = 0; i < 3; i++) {
101-
if (this.tokens[i]! < tokens[i]!) return -1
102-
if (this.tokens[i]! > tokens[i]!) return 1
103-
}
104-
return 0
105-
}
116+
interface SemverFn extends SemverFactory {
117+
(input: SemverInput): Semver
118+
}
106119

107-
toJSON = (): string => this.toString()
120+
const semverFactory = new SemverFactory()
108121

109-
toString(): string {
110-
return this.tokens.join('.')
111-
}
112-
}
122+
export const semver2 = semverFactory.of.bind(semverFactory) as SemverFn
113123

114-
/**
115-
* Shortcut for Semver.of(input)
116-
*/
117-
export function _semver(input: SemverInput): Semver {
118-
return Semver.of(input)
119-
}
124+
// The line below is the blackest of black magic I have ever written in 2024.
125+
// And probably 2023 as well.
126+
Object.setPrototypeOf(semver2, semverFactory)
120127

121128
/**
122129
* Returns 1 if a > b
@@ -127,7 +134,7 @@ export function _semver(input: SemverInput): Semver {
127134
*
128135
* Credit: https://stackoverflow.com/a/47159772/4919972
129136
*/
130-
export function _semverCompare(a: string, b: string): -1 | 0 | 1 {
137+
export function _quickSemverCompare(a: string, b: string): -1 | 0 | 1 {
131138
const t1 = a.split('.')
132139
const t2 = b.split('.')
133140
const s1 = _range(3)

0 commit comments

Comments
 (0)