Skip to content

Commit b760e42

Browse files
committed
comparisons
1 parent df56ca8 commit b760e42

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

semver/semver.go

+132
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"fmt"
1919
"regexp"
2020
"strconv"
21+
"strings"
2122
)
2223

2324
// See: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
@@ -85,6 +86,137 @@ func (v Version) IsZero() bool {
8586
return v.Major == 0 && v.Minor == 0 && v.Patch == 0 && v.PreRelease == "" && v.BuildMeta == ""
8687
}
8788

89+
//===========================================================================
90+
// Comparison
91+
//===========================================================================
92+
93+
// Compare returns the precedence of version v compared to version o. See Compare
94+
// for more details about how precedence is determined.
95+
func (v Version) Compare(o Version) int {
96+
return Compare(v, o)
97+
}
98+
99+
// Compare returns the precedence of version a compared to version b. If a has a higher
100+
// precedence than b (a > b), the result is 1. If a has a lower precedence than b (a < b),
101+
// the result is -1. If they have the same precedence (a == b), the result is 0.
102+
// Note that BuildMeta is not factored into precedence so it is possible to have two
103+
// semantically equivalent versions with different string values.
104+
//
105+
// Precedence is determined by the first difference when comparing each of these
106+
// identifiers from left to right as follows: Major, minor, and patch versions are
107+
// always compared numerically.
108+
//
109+
// When major, minor, and patch are equal, a pre-release version has lower precedence
110+
// than a normal version.
111+
//
112+
// Precedence for two pre-release versions with the same major, minor, and patch version
113+
// MUST be determined by comparing each dot separated identifier from left to right
114+
// until a difference is found as follows:
115+
//
116+
// 1. Identifiers consisting of only digits are compared numerically.
117+
// 2. Identifiers with letters or hyphens are compared lexically in ASCII sort order.
118+
// 3. Numeric identifiers always have lower precedence than non-numeric identifiers.
119+
// 4. A larger set of pre-release fields has a higher precedence than a smaller set,
120+
// if all of the preceding identifiers are equal.
121+
func Compare(a, b Version) int {
122+
if a.Major != b.Major {
123+
if a.Major > b.Major {
124+
return 1
125+
}
126+
return -1
127+
}
128+
129+
if a.Minor != b.Minor {
130+
if a.Minor > b.Minor {
131+
return 1
132+
}
133+
return -1
134+
}
135+
136+
if a.Patch != b.Patch {
137+
if a.Patch > b.Patch {
138+
return 1
139+
}
140+
return -1
141+
}
142+
143+
if a.PreRelease != b.PreRelease {
144+
// a doesn't have a prerelease so it has a higher precedence
145+
if a.PreRelease == "" {
146+
return 1
147+
}
148+
149+
// b doesn't have a prerelease so it has a higher precedence
150+
if b.PreRelease == "" {
151+
return -1
152+
}
153+
154+
ar := strings.Split(a.PreRelease, ".")
155+
br := strings.Split(b.PreRelease, ".")
156+
157+
if len(ar) >= len(br) {
158+
for i, bv := range br {
159+
av := ar[i]
160+
if precedence := compareIdentifier(av, bv); precedence != 0 {
161+
return precedence
162+
}
163+
}
164+
165+
// a has more pre-release fields than b and all are equal so a has a higher precedence
166+
// if len(ar) == len(br) then the first difference would have been found in the loop
167+
return 1
168+
} else {
169+
for i, av := range ar {
170+
bv := br[i]
171+
if precedence := compareIdentifier(av, bv); precedence != 0 {
172+
return precedence
173+
}
174+
}
175+
176+
// b has more pre-release fields than a and all are equal so b has a higher precedence
177+
return -1
178+
}
179+
}
180+
181+
// At this point we know the versions are semantically equivalent.
182+
return 0
183+
}
184+
185+
func compareIdentifier(a, b string) int {
186+
if a == b {
187+
return 0
188+
}
189+
190+
// Determine if a and b are numeric or non-numeric.
191+
na, aIsNumeric := isNumeric(a)
192+
nb, bIsNumeric := isNumeric(b)
193+
194+
switch {
195+
// If both are numeric, compare numerically.
196+
case aIsNumeric && bIsNumeric:
197+
if na > nb {
198+
return 1
199+
}
200+
return -1
201+
// If one is numeric and the other is not, the numeric one has lower precedence.
202+
case aIsNumeric:
203+
return -1
204+
case bIsNumeric:
205+
return 1
206+
default:
207+
// Compare lexicographically in ASCII sort order.
208+
return strings.Compare(a, b)
209+
}
210+
}
211+
212+
func isNumeric(s string) (v uint16, ok bool) {
213+
n, err := strconv.ParseUint(s, 10, 16)
214+
if err != nil {
215+
return 0, false
216+
}
217+
return uint16(n), true
218+
}
219+
88220
//===========================================================================
89221
// Serialization and Deserialization
90222
//===========================================================================

semver/semver_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,39 @@ func TestMarshal(t *testing.T) {
152152
})
153153
}
154154

155+
func TestCompare(t *testing.T) {
156+
tests := []struct {
157+
a, b string
158+
expected int
159+
}{
160+
{"1.0.0", "1.0.0", 0},
161+
{"2.3.0", "2.3.0", 0},
162+
{"3.1.4", "3.1.4", 0},
163+
{"1.0.0", "1.0.1", -1},
164+
{"1.0.0", "1.1.0", -1},
165+
{"2.1.1", "2.1.0", 1},
166+
{"2.1.0", "2.0.0", 1},
167+
{"2.0.0", "1.0.0", 1},
168+
{"1.0.0-alpha", "1.0.0", -1},
169+
{"1.0.0-alpha", "1.0.0-alpha.1", -1},
170+
{"1.0.0-alpha.1", "1.0.0-alpha.beta", -1},
171+
{"1.0.0-alpha.beta", "1.0.0-beta", -1},
172+
{"1.0.0-beta", "1.0.0-beta.2", -1},
173+
{"1.0.0-beta.2", "1.0.0-beta.11", -1},
174+
{"1.0.0-beta.11", "1.0.0-rc.1", -1},
175+
{"1.0.0-rc.1", "1.0.0", -1},
176+
{"1.0.0", "1.0.0+20130313144700", 0},
177+
{"1.0.0+20130313144700", "1.0.0+exp.sha.5114f85", 0},
178+
}
179+
180+
for i, tc := range tests {
181+
a := MustParse(tc.a)
182+
b := MustParse(tc.b)
183+
assert.Equal(t, tc.expected, a.Compare(b), "test case %d failed", i)
184+
assert.Equal(t, -1*tc.expected, Compare(b, a), "test case %d failed", i)
185+
}
186+
}
187+
155188
func TestSQL(t *testing.T) {
156189
t.Run("Scan", func(t *testing.T) {
157190
a := Version{}

0 commit comments

Comments
 (0)