Skip to content

Commit ffc4442

Browse files
committed
Added string-sorting support
1 parent 6ae0217 commit ffc4442

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed

version.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,77 @@ func (v *Version) CompatibleWith(u *Version) bool {
416416
}
417417
return compareNumber(vPatch, uPatch) == 0
418418
}
419+
420+
// SortableString returns the version encoded as a string that when compared
421+
// with alphanumeric ordering it respects the original semver ordering:
422+
//
423+
// (v1.SortableString() < v2.SortableString()) == v1.LessThan(v2)
424+
// cmp.Compare[string](v1.SortableString(), v2.SortableString()) == v1.CompareTo(v2)
425+
//
426+
// This may turn out useful when the version is saved in a database or is
427+
// introduced in a system that doesn't support semver ordering.
428+
func (v *Version) SortableString() string {
429+
// Encode a number in a string that when compared as string it respects
430+
// the original numeric order.
431+
// To allow longer numbers to be compared correctly, a prefix of ":"s
432+
// with the length of the number is added minus 1.
433+
// For example: 123 -> "::123"
434+
// 45 -> ":45"
435+
// The number written as string compare as ("123" < "99") but the encoded
436+
// version keeps the original integer ordering ("::123" > ":99").
437+
encodeNumber := func(in []byte) string {
438+
if len(in) == 0 {
439+
return "0"
440+
}
441+
p := ""
442+
for range in {
443+
p += ":"
444+
}
445+
return p[:len(p)-1] + string(in)
446+
}
447+
448+
var vMajor, vMinor, vPatch []byte
449+
vMajor = v.bytes[:v.major]
450+
if v.minor > v.major {
451+
vMinor = v.bytes[v.major+1 : v.minor]
452+
}
453+
if v.patch > v.minor {
454+
vPatch = v.bytes[v.minor+1 : v.patch]
455+
}
456+
457+
res := encodeNumber(vMajor) + "." + encodeNumber(vMinor) + "." + encodeNumber(vPatch)
458+
// If there is no pre-release, add a ";" to the end, otherwise add a "-" followed by the pre-release.
459+
// This ensure the correct ordering of the pre-release versions (that are always lower than the normal versions).
460+
if v.prerelease == v.patch {
461+
return res + ";"
462+
}
463+
res += "-"
464+
465+
isAlpha := false
466+
add := func(in []byte) {
467+
// if the pre-release piece is alphanumeric, add a ";" before the piece
468+
// otherwise add an ":" before the piece. This ensure the correct ordering
469+
// of the pre-release piece (numeric are lower than alphanumeric).
470+
if isAlpha {
471+
res += ";" + string(in)
472+
} else {
473+
res += ":" + encodeNumber(in)
474+
}
475+
isAlpha = false
476+
}
477+
prerelease := v.bytes[v.patch+1 : v.prerelease]
478+
start := 0
479+
for curr, c := range prerelease {
480+
if c == '.' {
481+
add(prerelease[start:curr])
482+
res += "."
483+
start = curr + 1
484+
continue
485+
}
486+
if !isNumeric(c) {
487+
isAlpha = true
488+
}
489+
}
490+
add(prerelease[start:])
491+
return res
492+
}

version_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package semver
88

99
import (
10+
"cmp"
1011
"fmt"
1112
"testing"
1213

@@ -54,6 +55,41 @@ func ascending(t *testing.T, allowEqual bool, list ...string) {
5455
require.True(t, b.GreaterThan(a))
5556
}
5657
}
58+
59+
for i := range list[0 : len(list)-1] {
60+
a := MustParse(list[i]).SortableString()
61+
b := MustParse(list[i+1]).SortableString()
62+
comp := cmp.Compare(a, b)
63+
if allowEqual {
64+
fmt.Printf("%s %s= %s\n", list[i], sign[comp], list[i+1])
65+
require.LessOrEqual(t, comp, 0)
66+
require.True(t, a <= b)
67+
require.False(t, a > b)
68+
} else {
69+
fmt.Printf("%s %s %s\n", list[i], sign[comp], list[i+1])
70+
require.Equal(t, comp, -1, "cmp(%s, %s) (%s, %s) must return '<', but returned '%s'", list[i], list[i+1], a, b, sign[comp])
71+
require.True(t, a < b)
72+
require.True(t, a <= b)
73+
require.False(t, a == b)
74+
require.False(t, a >= b)
75+
require.False(t, a > b)
76+
}
77+
78+
comp = cmp.Compare(b, a)
79+
fmt.Printf("%s %s %s\n", b, sign[comp], a)
80+
if allowEqual {
81+
require.GreaterOrEqual(t, comp, 0, "cmp(%s, %s) must return '>=', but returned '%s'", b, a, sign[comp])
82+
require.False(t, b < a)
83+
require.True(t, b >= a)
84+
} else {
85+
require.Equal(t, comp, 1)
86+
require.False(t, b < a)
87+
require.False(t, b <= a)
88+
require.False(t, b == a)
89+
require.True(t, b >= a)
90+
require.True(t, b > a)
91+
}
92+
}
5793
}
5894

5995
func TestVersionComparator(t *testing.T) {
@@ -124,6 +160,10 @@ func TestVersionComparator(t *testing.T) {
124160
"17.3.0-atmel3a.16.1-arduino7",
125161
"17.3.0-atmel3a.16.12-arduino7",
126162
"17.3.0-atmel3a.16.2-arduino7",
163+
"34.0.0",
164+
"51.0.0",
165+
"99.0.0",
166+
"123.0.0",
127167
)
128168
equal(
129169
MustParse(""),

0 commit comments

Comments
 (0)