Initial commit; import from github.com/lwithers/pkg
This commit is contained in:
commit
9b0ccf41c2
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2020 Laurence Withers
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,16 @@
|
||||||
|
# src.lwithers.me.uk/go/versionsort
|
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/src.lwithers.me.uk/go/versionsort?status.svg)](https://godoc.org/src.lwithers.me.uk/go/versionsort)
|
||||||
|
|
||||||
|
This package provides a Go implementation of the venerable C function
|
||||||
|
`versionsort(3)`. This lets you sort strings containing natural numbers, such
|
||||||
|
as versions. It understands that `1.12` is greater than `1.6`, for example.
|
||||||
|
|
||||||
|
Example of usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
input := []string{"1.12", "1.24", "1.1", "1.6"}
|
||||||
|
versionsort.Strings(input)
|
||||||
|
fmt.Println(input)
|
||||||
|
// Output: [1.1 1.6 1.12 1.24]
|
||||||
|
```
|
|
@ -0,0 +1,22 @@
|
||||||
|
package versionsort_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"src.lwithers.me.uk/go/versionsort"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleStrings() {
|
||||||
|
input := []string{"1.12", "1.24", "1.1", "1.6"}
|
||||||
|
versionsort.Strings(input)
|
||||||
|
fmt.Println(input)
|
||||||
|
// Output: [1.1 1.6 1.12 1.24]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleVersions() {
|
||||||
|
input := versionsort.Versions{"1.12", "1.24", "1.1", "1.6"}
|
||||||
|
sort.Sort(input)
|
||||||
|
fmt.Println(input)
|
||||||
|
// Output: [1.1 1.6 1.12 1.24]
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package versionsort
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLess(t *testing.T) {
|
||||||
|
// ensure v1<v2 (and that !(v2<v1) — so tc cannot contain v1==v2!)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
v1, v2 string
|
||||||
|
}{
|
||||||
|
{"1.1", "1.2"},
|
||||||
|
{"1.2", "1.10"},
|
||||||
|
{"x100", "x101"},
|
||||||
|
{"x99", "x100"},
|
||||||
|
{"x100x", "x101x"},
|
||||||
|
{"x99x", "x100x"},
|
||||||
|
{"100000000000000000000000000000000", "100000000000000000000000000000001"},
|
||||||
|
{"abc", "xyz"},
|
||||||
|
{"x1", "x11"},
|
||||||
|
{"1.1.1", "1.1.2"},
|
||||||
|
{"1.2.1", "1.2.3"},
|
||||||
|
{"1.2.3", "1.3.1"},
|
||||||
|
} {
|
||||||
|
t.Run(fmt.Sprintf("%s<%s", tc.v1, tc.v2), func(t *testing.T) {
|
||||||
|
if !Less(tc.v1, tc.v2) {
|
||||||
|
t.Errorf("%q<%q: got false, expected true",
|
||||||
|
tc.v1, tc.v2)
|
||||||
|
}
|
||||||
|
if Less(tc.v2, tc.v1) {
|
||||||
|
t.Errorf("%q<%q: got true, expected false",
|
||||||
|
tc.v2, tc.v1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// for v1==v2, ensure that !(v1<v2)
|
||||||
|
for _, tc := range []string{
|
||||||
|
"1", "1.1", "x1", "x100", "x100x", "xx",
|
||||||
|
} {
|
||||||
|
t.Run(fmt.Sprintf("%s==%s", tc, tc), func(t *testing.T) {
|
||||||
|
if Less(tc, tc) {
|
||||||
|
t.Error("got true, expected false")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegerStartEnd(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
v string
|
||||||
|
start int
|
||||||
|
expS, expE int
|
||||||
|
}{
|
||||||
|
{"0", 0, 0, 1},
|
||||||
|
{"00", 0, 1, 2},
|
||||||
|
{"0x", 0, 0, 1},
|
||||||
|
{"00x", 0, 1, 2},
|
||||||
|
{"1", 0, 0, 1},
|
||||||
|
{"11", 0, 0, 2},
|
||||||
|
{"1x", 0, 0, 1},
|
||||||
|
{"11x", 0, 0, 2},
|
||||||
|
{"01", 0, 1, 2},
|
||||||
|
{"01x", 0, 1, 2},
|
||||||
|
{"x1", 1, 1, 2},
|
||||||
|
{"x1x", 1, 1, 2},
|
||||||
|
} {
|
||||||
|
t.Run(tc.v, func(t *testing.T) {
|
||||||
|
s, e := integerStartEnd(tc.v, tc.start)
|
||||||
|
if s != tc.expS || e != tc.expE {
|
||||||
|
t.Errorf("got (%d,%d), expected (%d,%d)",
|
||||||
|
s, e, tc.expS, tc.expE)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSort(t *testing.T) {
|
||||||
|
exp := []string{
|
||||||
|
"1",
|
||||||
|
"1.1",
|
||||||
|
"1.2",
|
||||||
|
"2.9",
|
||||||
|
"2.10",
|
||||||
|
"2.11",
|
||||||
|
"11",
|
||||||
|
"12",
|
||||||
|
"x",
|
||||||
|
}
|
||||||
|
|
||||||
|
for round := 0; round < 100; round++ {
|
||||||
|
order := rand.Perm(len(exp))
|
||||||
|
var perm []string
|
||||||
|
for _, idx := range order {
|
||||||
|
perm = append(perm, exp[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
Strings(perm)
|
||||||
|
ok := true
|
||||||
|
for i := range exp {
|
||||||
|
if perm[i] != exp[i] {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
for i := range exp {
|
||||||
|
t.Errorf("%d: exp %q, act %q",
|
||||||
|
i, exp[i], perm[i])
|
||||||
|
}
|
||||||
|
t.Fatalf("out of order on round %d", round)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue