Initial commit; import from github.com/lwithers/pkg

This commit is contained in:
Laurence Withers 2020-01-03 14:03:25 +00:00
commit 9b0ccf41c2
6 changed files with 297 additions and 0 deletions

19
LICENSE Normal file
View File

@ -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.

16
README.md Normal file
View File

@ -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]
```

22
example_test.go Normal file
View File

@ -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]
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module src.lwithers.me.uk/go/versionsort
go 1.13

121
sort.go Normal file
View File

@ -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
}

116
unit_test.go Normal file
View File

@ -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)
}
}
}