versionsort/sort.go

122 lines
2.7 KiB
Go
Raw Permalink Normal View History

/*
Package versionsort implements a version-aware lexicographical sort.
For example, it knows that "1.2" is in fact less than "1.10".
*/
package versionsort
import "sort"
// Versions can be sorted.
type Versions []string
func (v Versions) Len() int {
return len(v)
}
func (v Versions) Less(i, j int) bool {
return Less(v[i], v[j])
}
func (v Versions) Swap(i, j int) {
v[i], v[j] = v[j], v[i]
}
// Sort an array of strings using version sort.
func Strings(versions []string) {
v := Versions(versions)
sort.Sort(v)
}
// Less returns true if v1 < v2, else false.
func Less(v1, v2 string) bool {
if v1 == v2 {
return false
}
// vStart is set when:
// - strings are equal up to pos
// - v1[pos-1] is not an integer
// - v1[pos] is an integer
// i.e. it should point to the start of any version number
vStart := -1
smallestLen := len(v1)
if len(v2) < smallestLen {
smallestLen = len(v2)
}
// iterate over each byte, breaking if we differ or reach the end of
// the string — and keep vStart updated
var pos int
for pos = 0; pos < smallestLen; pos++ {
if v1[pos] != v2[pos] {
break
}
// strings are equal thus far
if vStart == -1 {
// if we are not inside an integer, check for one starting
if v1[pos] >= '0' && v1[pos] <= '9' {
vStart = pos
}
} else {
// if we are inside an integer, check for it finishing
if v1[pos] < '0' || v1[pos] > '9' {
vStart = -1
}
}
}
// test for non-version-number differences
switch {
case pos == len(v1):
// v1 is a prefix of v2
return true
case pos == len(v2):
// v2 is a prefix of v1
return false
case vStart == -1 && (v1[pos] < '0' || v1[pos] > '9' || v2[pos] < '0' || v2[pos] > '9'):
// a non-numeric difference occurred, so fall back to standard
// lexicographical comparison
return v1 < v2
}
// extract position of integer fragment
v1s, v1e := integerStartEnd(v1, pos)
v2s, v2e := integerStartEnd(v2, pos)
// test for the smaller version number
switch {
// if version in v1 has a larger magnitude, it is necessarily greater
case (v1e - v1s) > (v2e - v2s):
return false
// similarly if v2 has larger magnitude, v1 is the smallest
case (v1e - v1s) < (v2e - v2s):
return true
// otherwise, same magnitude, so a lexicographical comparison of the
// numeric fragment will suffice
default:
return v1[v1s:v1e] < v2[v2s:v2e]
}
}
// integerStartEnd finds the start and end positions of an integer whose first
// byte lies at p. The start position may not equal p if there are leading
// zeroes.
func integerStartEnd(s string, p int) (start, end int) {
start = p
for end = p; end < len(s); end++ {
if s[end] < '0' || s[end] > '9' {
break
}
}
for start < end-1 && s[start] == '0' {
start++
}
return
}