commit 9b0ccf41c247502321cb18377f2c6a5e05246428 Author: Laurence Withers Date: Fri Jan 3 14:03:25 2020 +0000 Initial commit; import from github.com/lwithers/pkg diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c33580 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..571b17e --- /dev/null +++ b/README.md @@ -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] +``` diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..00ddca5 --- /dev/null +++ b/example_test.go @@ -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] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..30449d6 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module src.lwithers.me.uk/go/versionsort + +go 1.13 diff --git a/sort.go b/sort.go new file mode 100644 index 0000000..03960cc --- /dev/null +++ b/sort.go @@ -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 +} diff --git a/unit_test.go b/unit_test.go new file mode 100644 index 0000000..7bb90ad --- /dev/null +++ b/unit_test.go @@ -0,0 +1,116 @@ +package versionsort + +import ( + "fmt" + "math/rand" + "testing" +) + +func TestLess(t *testing.T) { + // ensure v1