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