From cc8cb655e908e08190a45bd069103fa8713de1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=A5=D1=80?= =?UTF-8?q?=D0=B0=D0=BC=D0=BE=D0=B2?= Date: Wed, 19 Jun 2024 21:45:56 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .directory | 6 ++ LICENSE | 21 +++++++ README.md | 4 ++ go.mod | 5 ++ go.sum | 2 + vercmp.go | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ vercmp_test.go | 64 +++++++++++++++++++++ 7 files changed, 250 insertions(+) create mode 100644 .directory create mode 100644 LICENSE create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 vercmp.go create mode 100644 vercmp_test.go diff --git a/.directory b/.directory new file mode 100644 index 0000000..3e22ec0 --- /dev/null +++ b/.directory @@ -0,0 +1,6 @@ +[Dolphin] +Timestamp=2024,6,19,21,39,30.811 +Version=4 + +[Settings] +HiddenFilesShown=true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..feb86d1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Elara Musayelyan + +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..beda8ab --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# vercmp +[![Go Reference](https://pkg.go.dev/badge/go.elara.ws/vercmp.svg)](https://pkg.go.dev/go.elara.ws/vercmp) + +This is a simple library that compares two versions using an algorithm loosely based on the `rpmvercmp` algorithm. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aa92edd --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module go.elara.ws/vercmp + +go 1.20 + +require golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..001575f --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= diff --git a/vercmp.go b/vercmp.go new file mode 100644 index 0000000..4cffc18 --- /dev/null +++ b/vercmp.go @@ -0,0 +1,148 @@ +// Package vercmp provides comparison between two arbitrary version strings. +// It uses a modified implementation of the rpmvercmp algorithm used by the RPM package manager. +package vercmp + +import ( + "strconv" + "strings" + + "golang.org/x/exp/slices" +) + +// Compare compares two version strings. +// It returns 1 if v1 is greater, +// 0 if the versions are equal, +// and -1 if v2 is greater +func Compare(v1, v2 string) int { + if v1 == v2 { + return 0 + } + + return sepVerCmp(sepLabel(v1), sepLabel(v2)) +} + +func sepVerCmp(e1, e2 []string) int { + if slices.Equal(e1, e2) { + return 0 + } + + // proc stores the amount of elements processed + proc := 0 + + for i := 0; i < len(e1); i++ { + proc++ + + if i >= len(e2) { + return 1 + } + + elem1 := e1[i] + elem2 := e2[i] + + if elem1 == elem2 { + continue + } + + if isNumElem(elem1) && isNumElem(elem2) { + elem1v, err := strconv.ParseInt(elem1, 10, 64) + if err != nil { + // error should never happen due to isNumElem() + panic(err) + } + + elem2v, err := strconv.ParseInt(elem2, 10, 64) + if err != nil { + // error should never happen due to isNumElem() + panic(err) + } + + if elem1v > elem2v { + return 1 + } else if elem1v < elem2v { + return -1 + } + } else if isNumElem(elem1) && isAlphaElem(elem2) { + return 1 + } else if isAlphaElem(elem1) && isNumElem(elem2) { + return -1 + } else if isAlphaElem(elem1) && isAlphaElem(elem2) { + if elem1 > elem2 { + return 1 + } else if elem1 < elem2 { + return -1 + } + } + } + + if proc < len(e2) { + return -1 + } + + return 0 +} + +func sepLabel(label string) []string { + const ( + other = iota + alpha + num + ) + + var ( + curType uint8 + out []string + sb strings.Builder + ) + + for _, char := range label { + if isNum(char) { + if curType != num && curType != other { + out = append(out, sb.String()) + sb.Reset() + } + + sb.WriteRune(char) + curType = num + } else if isAlpha(char) { + if curType != alpha && curType != other { + out = append(out, sb.String()) + sb.Reset() + } + + sb.WriteRune(char) + curType = alpha + } else { + if curType != other { + out = append(out, sb.String()) + sb.Reset() + } + curType = other + } + } + + if sb.Len() != 0 { + out = append(out, sb.String()) + } + + return out +} + +func isNumElem(s string) bool { + // Check only the first rune as all elements + // should consist of the same type of rune + return isNum([]rune(s[:1])[0]) +} + +func isNum(r rune) bool { + return r >= '0' && r <= '9' +} + +func isAlphaElem(s string) bool { + // Check only the first rune as all elements + // should consist of the same type of rune + return isAlpha([]rune(s[:1])[0]) +} + +func isAlpha(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') +} diff --git a/vercmp_test.go b/vercmp_test.go new file mode 100644 index 0000000..663263b --- /dev/null +++ b/vercmp_test.go @@ -0,0 +1,64 @@ +package vercmp + +import ( + "testing" + + "golang.org/x/exp/slices" +) + +func TestSepLabel(t *testing.T) { + type item struct { + label string + expected []string + } + + table := []item{ + {"2.0.1", []string{"2", "0", "1"}}, + {"v0.0.1", []string{"v", "0", "0", "1"}}, + {"2xFg33.+f.5", []string{"2", "xFg", "33", "f", "5"}}, + } + + for _, it := range table { + t.Run(it.label, func(t *testing.T) { + s := sepLabel(it.label) + if !slices.Equal(s, it.expected) { + t.Errorf("Expected %v, got %v", it.expected, s) + } + }) + } +} + +func TestVerCmp(t *testing.T) { + type item struct { + v1, v2 string + expected int + } + + table := []item{ + {"1.0010", "1.9", 1}, + {"1.05", "1.5", 0}, + {"1.0", "1", 1}, + {"1", "1.0", -1}, + {"2.50", "2.5", 1}, + {"FC5", "fc4", -1}, + {"2a", "2.0", -1}, + {"1.0", "1.fc4", 1}, + {"3.0.0_fc", "3.0.0.fc", 0}, + {"4.1__", "4.1+", 0}, + } + + for _, it := range table { + t.Run(it.v1+"/"+it.v2, func(t *testing.T) { + c := Compare(it.v1, it.v2) + if c != it.expected { + t.Errorf("Expected %d, got %d", it.expected, c) + } + + // Ensure opposite comparison gives opposite value + c = -Compare(it.v2, it.v1) + if c != it.expected { + t.Errorf("Expected %d, got %d (opposite)", it.expected, c) + } + }) + } +}