diff --git a/benchmark_test.go b/benchmark_test.go new file mode 100644 index 0000000..1dbfe87 --- /dev/null +++ b/benchmark_test.go @@ -0,0 +1,119 @@ +// +// Copyright 2018-2023 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package semver + +import ( + "testing" +) + +var list = []string{ + "0.0.1-rc.0", // 0 + "0.0.1-rc.0+build", // 1 + "0.0.1-rc.1", // 2 + "0.0.1", // 3 + "0.0.1+build", // 4 + "0.0.2-rc.1", // 5 - BREAKING CHANGE + "0.0.2-rc.1+build", // 6 + "0.0.2", // 7 + "0.0.2+build", // 8 + "0.0.3-rc.1", // 9 - BREAKING CHANGE + "0.0.3-rc.2", // 10 + "0.0.3", // 11 + "0.1.0", // 12 - BREAKING CHANGE + "0.3.3-rc.0", // 13 - BREAKING CHANGE + "0.3.3-rc.1", // 14 + "0.3.3", // 15 + "0.3.3+build", // 16 + "0.3.4-rc.1", // 17 + "0.3.4", // 18 + "0.4.0", // 19 - BREAKING CHANGE + "1.0.0-rc", // 20 - BREAKING CHANGE + "1.0.0", // 21 + "1.0.0+build", // 22 + "1.2.1-rc", // 23 + "1.2.1", // 24 + "1.2.1+build", // 25 + "1.2.3-rc.2", // 26 + "1.2.3-rc.2+build", // 27 + "1.2.3", // 28 + "1.2.3+build", // 29 + "1.2.4", // 30 + "1.3.0-rc.0+build", // 31 + "1.3.0", // 32 + "1.3.0+build", // 33 + "1.3.1-rc.0", // 34 + "1.3.1-rc.1", // 35 + "1.3.1", // 36 + "1.3.5", // 37 + "2.0.0-rc", // 38 - BREAKING CHANGE + "2.0.0-rc+build", // 39 + "2.0.0", // 40 + "2.0.0+build", // 41 + "2.1.0-rc", // 42 + "2.1.0-rc+build", // 43 + "2.1.0", // 44 + "2.1.0+build", // 45 + "2.1.3-rc", // 46 + "2.1.3", // 47 + "2.3.0", // 48 + "2.3.1", // 49 + "3.0.0", // 50 - BREAKING CHANGE +} + +func BenchmarkVersionParser(b *testing.B) { + res := &Version{} + for i := 0; i < b.N; i++ { + for _, v := range list { + res.raw = v + res.bytes = []byte(v) + parse(res) + } + } + + // $ go test -benchmem -run=^$ -bench ^BenchmarkVersionParser$ go.bug.st/relaxed-semver + // goos: linux + // goarch: amd64 + // pkg: go.bug.st/relaxed-semver + // cpu: AMD Ryzen 5 3600 6-Core Processor + + // Results for v0.11.0: + // BenchmarkVersionParser-12 188611 7715 ns/op 8557 B/op 51 allocs/op + + // Results for v0.12.0: \o/ + // BenchmarkVersionParser-12 479626 3719 ns/op 616 B/op 51 allocs/op +} + +func BenchmarkVersionComparator(b *testing.B) { + b.StopTimer() + vList := []*Version{} + for _, in := range list { + vList = append(vList, MustParse(in)) + } + l := len(vList) + b.StartTimer() + + for i := 0; i < b.N; i++ { + // cross compare all versions + for x := 0; x < l; x++ { + for y := 0; y < l; y++ { + vList[x].CompareTo(vList[y]) + } + } + } + + // $ go test -benchmem -run=^$ -bench ^BenchmarkVersionComparator$ go.bug.st/relaxed-semver -v + // goos: linux + // goarch: amd64 + // pkg: go.bug.st/relaxed-semver + // cpu: AMD Ryzen 5 3600 6-Core Processor + + // Results for v0.11.0: + // BenchmarkVersionComparator-12 74793 17347 ns/op 0 B/op 0 allocs/op + + // Results for v0.12.0: :-D + // BenchmarkVersionComparator-12 101772 11720 ns/op 0 B/op 0 allocs/op +} diff --git a/binary.go b/binary.go index 3c19cdf..141fb2d 100644 --- a/binary.go +++ b/binary.go @@ -19,34 +19,22 @@ func marshalByteArray(b []byte) []byte { return res } -func marshalInt(i int) []byte { - res := make([]byte, 4) - binary.BigEndian.PutUint32(res, uint32(i)) - return res -} - // MarshalBinary implements binary custom encoding func (v *Version) MarshalBinary() ([]byte, error) { + // TODO could be preallocated without bytes.Buffer res := new(bytes.Buffer) - _, _ = res.Write(marshalByteArray(v.major)) - _, _ = res.Write(marshalByteArray(v.minor)) - _, _ = res.Write(marshalByteArray(v.patch)) - _, _ = res.Write(marshalInt(len(v.prerelases))) - for _, pre := range v.prerelases { - _, _ = res.Write(marshalByteArray(pre)) - } - _, _ = res.Write(marshalInt(len(v.numericPrereleases))) - for _, npre := range v.numericPrereleases { - v := []byte{0} - if npre { - v[0] = 1 - } - _, _ = res.Write(v) - } - _, _ = res.Write(marshalInt(len(v.builds))) - for _, build := range v.builds { - _, _ = res.Write(marshalByteArray(build)) - } + intBuff := [4]byte{} + _, _ = res.Write(marshalByteArray([]byte(v.raw))) + binary.BigEndian.PutUint32(intBuff[:], uint32(v.major)) + _, _ = res.Write(intBuff[:]) + binary.BigEndian.PutUint32(intBuff[:], uint32(v.minor)) + _, _ = res.Write(intBuff[:]) + binary.BigEndian.PutUint32(intBuff[:], uint32(v.patch)) + _, _ = res.Write(intBuff[:]) + binary.BigEndian.PutUint32(intBuff[:], uint32(v.prerelease)) + _, _ = res.Write(intBuff[:]) + binary.BigEndian.PutUint32(intBuff[:], uint32(v.build)) + _, _ = res.Write(intBuff[:]) return res.Bytes(), nil } @@ -63,31 +51,14 @@ func decodeInt(data []byte) (int, []byte) { func (v *Version) UnmarshalBinary(data []byte) error { var buff []byte - v.major, data = decodeArray(data) - v.minor, data = decodeArray(data) - v.patch, data = decodeArray(data) - n, data := decodeInt(data) - v.prerelases = nil - for i := 0; i < n; i++ { - buff, data = decodeArray(data) - v.prerelases = append(v.prerelases, buff) - } - v.numericPrereleases = nil - n, data = decodeInt(data) - for i := 0; i < n; i++ { - num := false - if data[0] == 1 { - num = true - } - v.numericPrereleases = append(v.numericPrereleases, num) - data = data[1:] - } - v.builds = nil - n, data = decodeInt(data) - for i := 0; i < n; i++ { - buff, data = decodeArray(data) - v.builds = append(v.builds, buff) - } + buff, data = decodeArray(data) + v.raw = string(buff) + v.bytes = []byte(v.raw) + v.major, data = decodeInt(data) + v.minor, data = decodeInt(data) + v.patch, data = decodeInt(data) + v.prerelease, data = decodeInt(data) + v.build, _ = decodeInt(data) return nil } diff --git a/binary_test.go b/binary_test.go index 2b21b91..666f964 100644 --- a/binary_test.go +++ b/binary_test.go @@ -20,8 +20,8 @@ func TestGOBEncoderVersion(t *testing.T) { v, err := Parse(testVersion) require.NoError(t, err) - dumpV := fmt.Sprintf("%s,%s,%s,%s,%v,%s", v.major, v.minor, v.patch, v.prerelases, v.numericPrereleases, v.builds) - require.Equal(t, "1,2,3,[aaa 4 5 6],[false true true true],[bbb 7 8 9]", dumpV) + dumpV := fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, v.major, v.minor, v.patch, v.prerelease, v.build) + require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9,1,3,5,15,25", dumpV) require.Equal(t, testVersion, v.String()) dataV := new(bytes.Buffer) @@ -31,10 +31,22 @@ func TestGOBEncoderVersion(t *testing.T) { var u Version err = gob.NewDecoder(dataV).Decode(&u) require.NoError(t, err) - dumpU := fmt.Sprintf("%s,%s,%s,%s,%v,%s", u.major, u.minor, u.patch, u.prerelases, u.numericPrereleases, u.builds) + dumpU := fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, u.major, u.minor, u.patch, u.prerelease, u.build) require.Equal(t, dumpV, dumpU) require.Equal(t, testVersion, u.String()) + + { + dataV := new(bytes.Buffer) + dataU := new(bytes.Buffer) + require.NoError(t, gob.NewEncoder(dataV).Encode(MustParse("1.6.2"))) + require.NoError(t, gob.NewEncoder(dataU).Encode(MustParse("1.6.3"))) + + var v, u *Version + require.NoError(t, gob.NewDecoder(dataV).Decode(&v)) + require.NoError(t, gob.NewDecoder(dataU).Decode(&u)) + require.True(t, u.GreaterThan(v)) + } } func TestGOBEncoderRelaxedVersion(t *testing.T) { diff --git a/json.go b/json.go index fd20940..c72e4b1 100644 --- a/json.go +++ b/json.go @@ -26,12 +26,13 @@ func (v *Version) UnmarshalJSON(data []byte) error { return err } + v.raw = parsed.raw + v.bytes = []byte(v.raw) v.major = parsed.major v.minor = parsed.minor v.patch = parsed.patch - v.prerelases = parsed.prerelases - v.numericPrereleases = parsed.numericPrereleases - v.builds = parsed.builds + v.prerelease = parsed.prerelease + v.build = parsed.build return nil } diff --git a/json_test.go b/json_test.go index 02c8116..0c4a4fb 100644 --- a/json_test.go +++ b/json_test.go @@ -26,11 +26,9 @@ func TestJSONParseVersion(t *testing.T) { var u Version err = json.Unmarshal(data, &u) require.NoError(t, err) - dump := fmt.Sprintf("%s,%s,%s,%s,%v,%s", - u.major, u.minor, u.patch, - u.prerelases, u.numericPrereleases, - u.builds) - require.Equal(t, "1,2,3,[aaa 4 5 6],[false true true true],[bbb 7 8 9]", dump) + dump := fmt.Sprintf("%v,%v,%v,%v,%v,%v", + u.raw, u.major, u.minor, u.patch, u.prerelease, u.build) + require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9,1,3,5,15,25", dump) require.Equal(t, testVersion, u.String()) err = json.Unmarshal([]byte(`"invalid"`), &u) @@ -38,6 +36,10 @@ func TestJSONParseVersion(t *testing.T) { err = json.Unmarshal([]byte(`123`), &u) require.Error(t, err) + + require.NoError(t, json.Unmarshal([]byte(`"1.6.2"`), &v)) + require.NoError(t, json.Unmarshal([]byte(`"1.6.3"`), &u)) + require.True(t, u.GreaterThan(v)) } func TestJSONParseRelaxedVersion(t *testing.T) { diff --git a/parser.go b/parser.go index 57d7de7..17eaba3 100644 --- a/parser.go +++ b/parser.go @@ -10,8 +10,6 @@ import ( "fmt" ) -var empty = []byte("") - // MustParse parse a version string and panic if the parsing fails func MustParse(inVersion string) *Version { res, err := Parse(inVersion) @@ -22,15 +20,20 @@ func MustParse(inVersion string) *Version { } // Parse parse a version string -func Parse(inVersioin string) (*Version, error) { +func Parse(inVersion string) (*Version, error) { result := &Version{ - major: empty[:], - minor: empty[:], - patch: empty[:], + raw: inVersion, + bytes: []byte(inVersion), + } + if err := parse(result); err != nil { + return nil, err } + return result, nil +} +func parse(result *Version) error { // Setup parsing harness - in := []byte(inVersioin) + in := result.bytes inLen := len(in) currIdx := -1 var curr byte @@ -51,110 +54,139 @@ func Parse(inVersioin string) (*Version, error) { // Parse major if !next() { - return result, nil // empty version + return nil // empty version } if !numeric[curr] { - return nil, fmt.Errorf("no major version found") + return fmt.Errorf("no major version found") } if curr == '0' { - result.major = in[0:1] // 0 + result.major = 1 if !next() { - return result, nil + result.minor = 1 + result.patch = 1 + result.prerelease = 1 + result.build = 1 + return nil } if numeric[curr] { - return nil, fmt.Errorf("major version must not be prefixed with zero") + return fmt.Errorf("major version must not be prefixed with zero") } if !versionSeparator[curr] { - return nil, fmt.Errorf("invalid major version separator '%c'", curr) + return fmt.Errorf("invalid major version separator '%c'", curr) } // Fallthrough and parse next element } else { for { if !next() { - result.major = in[0:currIdx] - return result, nil + result.major = currIdx + result.minor = currIdx + result.patch = currIdx + result.prerelease = currIdx + result.build = currIdx + return nil } if numeric[curr] { continue } if versionSeparator[curr] { - result.major = in[0:currIdx] + result.major = currIdx + result.minor = currIdx + result.patch = currIdx + result.prerelease = currIdx + result.build = currIdx break } - return nil, fmt.Errorf("invalid major version separator '%c'", curr) + return fmt.Errorf("invalid major version separator '%c'", curr) } } // Parse minor if curr == '.' { if !next() || !numeric[curr] { - return nil, fmt.Errorf("no minor version found") + return fmt.Errorf("no minor version found") } if curr == '0' { - result.minor = in[currIdx : currIdx+1] // x.0 + result.minor = currIdx + 1 if !next() { - return result, nil + result.patch = currIdx + result.prerelease = currIdx + result.build = currIdx + return nil } if numeric[curr] { - return nil, fmt.Errorf("minor version must not be prefixed with zero") + return fmt.Errorf("minor version must not be prefixed with zero") } if !versionSeparator[curr] { - return nil, fmt.Errorf("invalid minor version separator '%c'", curr) + return fmt.Errorf("invalid minor version separator '%c'", curr) } // Fallthrough and parse next element } else { - minorIdx := currIdx for { if !next() { - result.minor = in[minorIdx:currIdx] - return result, nil + result.minor = currIdx + result.patch = currIdx + result.prerelease = currIdx + result.build = currIdx + return nil } if numeric[curr] { continue } if versionSeparator[curr] { - result.minor = in[minorIdx:currIdx] + result.minor = currIdx + result.patch = currIdx + result.prerelease = currIdx + result.build = currIdx break } - return nil, fmt.Errorf("invalid minor version separator '%c'", curr) + return fmt.Errorf("invalid minor version separator '%c'", curr) } } + } else { + result.minor = currIdx } // Parse patch if curr == '.' { if !next() || !numeric[curr] { - return nil, fmt.Errorf("no patch version found") + return fmt.Errorf("no patch version found") } if curr == '0' { - result.patch = in[currIdx : currIdx+1] // x.y.0 + result.patch = currIdx + 1 if !next() { - return result, nil + result.prerelease = currIdx + result.build = currIdx + return nil } if numeric[curr] { - return nil, fmt.Errorf("patch version must not be prefixed with zero") + return fmt.Errorf("patch version must not be prefixed with zero") } if !versionSeparator[curr] { - return nil, fmt.Errorf("invalid patch version separator '%c'", curr) + return fmt.Errorf("invalid patch version separator '%c'", curr) } // Fallthrough and parse next element } else { - patchIdx := currIdx for { if !next() { - result.patch = in[patchIdx:currIdx] - return result, nil + result.patch = currIdx + result.prerelease = currIdx + result.build = currIdx + return nil } if numeric[curr] { continue } if curr == '-' || curr == '+' { - result.patch = in[patchIdx:currIdx] + result.patch = currIdx + result.prerelease = currIdx + result.build = currIdx break } - return nil, fmt.Errorf("invalid patch version separator '%c'", curr) + return fmt.Errorf("invalid patch version separator '%c'", curr) } } + } else { + result.patch = currIdx } // 9. A pre-release version MAY be denoted by appending a hyphen and a series @@ -176,15 +208,15 @@ func Parse(inVersioin string) (*Version, error) { for { if hasNext := next(); !hasNext || curr == '.' || curr == '+' { if prereleaseIdx == currIdx { - return nil, fmt.Errorf("empty prerelease not allowed") + return fmt.Errorf("empty prerelease not allowed") } if zeroPrefix && !alphaIdentifier && currIdx-prereleaseIdx > 1 { - return nil, fmt.Errorf("numeric prerelease must not be prefixed with zero") + return fmt.Errorf("numeric prerelease must not be prefixed with zero") } - result.prerelases = append(result.prerelases, in[prereleaseIdx:currIdx]) - result.numericPrereleases = append(result.numericPrereleases, !alphaIdentifier) + result.prerelease = currIdx if !hasNext { - return result, nil + result.build = currIdx + return nil } if curr == '+' { break @@ -207,8 +239,10 @@ func Parse(inVersioin string) (*Version, error) { alphaIdentifier = true continue } - return nil, fmt.Errorf("invalid prerelease separator: '%c'", curr) + return fmt.Errorf("invalid prerelease separator: '%c'", curr) } + } else { + result.prerelease = currIdx } // 10. Build metadata MAY be denoted by appending a plus sign and a series of @@ -226,11 +260,11 @@ func Parse(inVersioin string) (*Version, error) { for { if hasNext := next(); !hasNext || curr == '.' { if buildIdx == currIdx { - return nil, fmt.Errorf("empty build tag not allowed") + return fmt.Errorf("empty build tag not allowed") } - result.builds = append(result.builds, in[buildIdx:currIdx]) + result.build = currIdx if !hasNext { - return result, nil + return nil } // Multiple builds @@ -240,8 +274,40 @@ func Parse(inVersioin string) (*Version, error) { if identifier[curr] { continue } - return nil, fmt.Errorf("invalid separator for builds: '%c'", curr) + return fmt.Errorf("invalid separator for builds: '%c'", curr) } } - return nil, fmt.Errorf("invalid separator: '%c'", curr) + return fmt.Errorf("invalid separator: '%c'", curr) +} + +func (v *Version) majorString() string { + return v.raw[:v.major] +} + +func (v *Version) minorString() string { + if v.minor > v.major { + return v.raw[v.major+1 : v.minor] + } + return "" +} + +func (v *Version) patchString() string { + if v.patch > v.minor { + return v.raw[v.minor+1 : v.patch] + } + return "" +} + +func (v *Version) prereleaseString() string { + if v.prerelease > v.patch { + return v.raw[v.patch+1 : v.prerelease] + } + return "" +} + +func (v *Version) buildString() string { + if v.build > v.prerelease { + return v.raw[v.prerelease+1 : v.build] + } + return "" } diff --git a/parser_test.go b/parser_test.go index 91a09cf..f55936b 100644 --- a/parser_test.go +++ b/parser_test.go @@ -14,28 +14,22 @@ import ( ) func TestParser(t *testing.T) { + MustParse("").CompareTo(MustParse("0+aaa")) valid := func(in, normalized, expectedDump string) { v, err := Parse(in) require.NoError(t, err, "parsing '%s'", in) require.Equal(t, in, v.String(), "printing of '%s'", in) require.Equal(t, normalized, string(v.NormalizedString()), "normalized printing of '%s'", in) - dump := string(v.major) + "," - dump += string(v.minor) + "," - dump += string(v.patch) - for i, prerelease := range v.prerelases { - dump += "," - if v.numericPrereleases[i] { - dump += "N" - } - dump += string(prerelease) - } - for _, build := range v.builds { - dump += ";" + string(build) - } + dump := fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, v.majorString(), v.minorString(), v.patchString(), v.prereleaseString(), v.buildString()) require.Equal(t, expectedDump, dump, "fields of parsed '%s'", in) fmt.Printf("%s -> %s\n", in, v.String()) v.Normalize() require.Equal(t, normalized, v.String(), "normalization of '%s'", in) + vn, err := Parse(normalized) + require.NoError(t, err) + dump = fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, v.majorString(), v.minorString(), v.patchString(), v.prereleaseString(), v.buildString()) + dumpNormalized := fmt.Sprintf("%v,%v,%v,%v,%v,%v", vn.raw, vn.majorString(), vn.minorString(), vn.patchString(), vn.prereleaseString(), vn.buildString()) + require.Equal(t, dumpNormalized, dump) } invalid := func(in string) { v, err := Parse(in) @@ -44,155 +38,162 @@ func TestParser(t *testing.T) { fmt.Printf("%s -> %s\n", in, err) } - valid("", "0.0.0", ",,") - invalid("0.0.0.0") - invalid("a") - invalid(".") - invalid("-ab") - invalid("+ab") - valid("0", "0.0.0", "0,,") - valid("0.0.0", "0.0.0", "0,0,0") - valid("1", "1.0.0", "1,,") - valid("1.0.0", "1.0.0", "1,0,0") - valid("14", "14.0.0", "14,,") - valid("123456789123456789123456789", "123456789123456789123456789.0.0", "123456789123456789123456789,,") - invalid("12ab") - invalid("01") - invalid("0ab") - invalid(".1.1") - invalid("1-") - valid("1-0", "1.0.0-0", "1,,,N0") - valid("1-pre", "1.0.0-pre", "1,,,pre") - valid("1-pre.a", "1.0.0-pre.a", "1,,,pre,a") - valid("1-pre.a.0", "1.0.0-pre.a.0", "1,,,pre,a,N0") - valid("1-pre.0.a", "1.0.0-pre.0.a", "1,,,pre,N0,a") - valid("1-pre.a.10", "1.0.0-pre.a.10", "1,,,pre,a,N10") - invalid("1-pre.a.01") - invalid("1-pre.a..1") - invalid("1-pre.a.01.1") - invalid("1-pre.a.01*.1") - valid("1+build3", "1.0.0+build3", "1,,;build3") - invalid("1+build3+build2") - valid("1+build3.123.001", "1.0.0+build3.123.001", "1,,;build3;123;001") - invalid("1+build3.123..001") - invalid("1+build3.123*.001") - valid("1-0+build3", "1.0.0-0+build3", "1,,,N0;build3") - valid("1-pre+build3", "1.0.0-pre+build3", "1,,,pre;build3") - valid("1-pre.a+build3", "1.0.0-pre.a+build3", "1,,,pre,a;build3") - valid("1-pre.a.10+build3", "1.0.0-pre.a.10+build3", "1,,,pre,a,N10;build3") - invalid("1-pre.a.01+build3") - invalid("1-pre.a..1+build3") - invalid("1-pre.a.01.1+build3") - invalid("1-pre.a.01*.1+build3") - valid("1-0+build3.123.001", "1.0.0-0+build3.123.001", "1,,,N0;build3;123;001") - valid("1-pre+build3.123.001", "1.0.0-pre+build3.123.001", "1,,,pre;build3;123;001") - valid("1-pre.a+build3.123.001", "1.0.0-pre.a+build3.123.001", "1,,,pre,a;build3;123;001") - valid("1-pre.a.0+build3.123.001", "1.0.0-pre.a.0+build3.123.001", "1,,,pre,a,N0;build3;123;001") - valid("1-pre.0.a+build3.123.001", "1.0.0-pre.0.a+build3.123.001", "1,,,pre,N0,a;build3;123;001") - valid("1-pre.a.10+build3.123.001", "1.0.0-pre.a.10+build3.123.001", "1,,,pre,a,N10;build3;123;001") - invalid("1-pre.a.+build3.123.001") - invalid("1-pre.a.01+build3.123.001") - invalid("1-pre.a.01*+build3.123.001") - - invalid("1.") - invalid("1.a") - invalid("1..2") - valid("1.2", "1.2.0", "1,2,") - valid("1.0", "1.0.0", "1,0,") - invalid("1.02") - invalid("1.0ab") - invalid("1.12ab") - valid("1.123456789123456789123456789", "1.123456789123456789123456789.0", "1,123456789123456789123456789,") - invalid("1.2-") - valid("1.2-0", "1.2.0-0", "1,2,,N0") - valid("1.2-pre", "1.2.0-pre", "1,2,,pre") - valid("1.2-pre.a", "1.2.0-pre.a", "1,2,,pre,a") - valid("1.2-pre.a.0", "1.2.0-pre.a.0", "1,2,,pre,a,N0") - valid("1.2-pre.0.a", "1.2.0-pre.0.a", "1,2,,pre,N0,a") - valid("1.2-pre.a.10", "1.2.0-pre.a.10", "1,2,,pre,a,N10") - valid("1.2-pre.a.10", "1.2.0-pre.a.10", "1,2,,pre,a,N10") - invalid("1.2-pre.a.01") - invalid("1.2-pre.a..1") - invalid("1.2-pre.a.01.1") - invalid("1.2-pre.a.01*.1") - - valid("1.2+build3", "1.2.0+build3", "1,2,;build3") - valid("1.2-0+build3", "1.2.0-0+build3", "1,2,,N0;build3") - invalid("1.2+build3+build2") - valid("1.2+build3.123.001", "1.2.0+build3.123.001", "1,2,;build3;123;001") - invalid("1.2+build3.123..001") - invalid("1.2+build3.123*.001") - valid("1.2-pre+build3", "1.2.0-pre+build3", "1,2,,pre;build3") - valid("1.2-pre.a.0+build3", "1.2.0-pre.a.0+build3", "1,2,,pre,a,N0;build3") - valid("1.2-pre.0.a+build3", "1.2.0-pre.0.a+build3", "1,2,,pre,N0,a;build3") - valid("1.2-pre.a.10+build3", "1.2.0-pre.a.10+build3", "1,2,,pre,a,N10;build3") - valid("1.2-pre.a+build3", "1.2.0-pre.a+build3", "1,2,,pre,a;build3") - valid("1.2-pre.a.10+build3", "1.2.0-pre.a.10+build3", "1,2,,pre,a,N10;build3") - invalid("1.2-pre.a.01+build3") - invalid("1.2-pre.a..1+build3") - invalid("1.2-pre.a.01.1+build3") - invalid("1.2-pre.a.01*.1+build3") - valid("1.2-0+build3.123.001", "1.2.0-0+build3.123.001", "1,2,,N0;build3;123;001") - valid("1.2-pre+build3.123.001", "1.2.0-pre+build3.123.001", "1,2,,pre;build3;123;001") - valid("1.2-pre.a+build3.123.001", "1.2.0-pre.a+build3.123.001", "1,2,,pre,a;build3;123;001") - valid("1.2-pre.a.0+build3.123.001", "1.2.0-pre.a.0+build3.123.001", "1,2,,pre,a,N0;build3;123;001") - valid("1.2-pre.0.a+build3.123.001", "1.2.0-pre.0.a+build3.123.001", "1,2,,pre,N0,a;build3;123;001") - valid("1.2-pre.a.10+build3.123.001", "1.2.0-pre.a.10+build3.123.001", "1,2,,pre,a,N10;build3;123;001") - valid("1.2-pre.a.10+build3.123.001", "1.2.0-pre.a.10+build3.123.001", "1,2,,pre,a,N10;build3;123;001") - invalid("1.2-pre.a.+build3.123.001") - invalid("1.2-pre.a.01+build3.123.001") - invalid("1.2-pre.a.01*+build3.123.001") - - invalid("1.2.a") - invalid("1.2.") - valid("1.2.3", "1.2.3", "1,2,3") - valid("1.2.0", "1.2.0", "1,2,0") - invalid("1.2.03") - invalid("1.2.0ab") - invalid("1.2.34ab") - valid("1.2.123456789123456789123456789", "1.2.123456789123456789123456789", "1,2,123456789123456789123456789") - invalid("1.2.3-") - valid("1.2.3-0", "1.2.3-0", "1,2,3,N0") - valid("1.2.3-pre", "1.2.3-pre", "1,2,3,pre") - valid("1.2.3-pre.a", "1.2.3-pre.a", "1,2,3,pre,a") - valid("1.2.3-pre.a.0", "1.2.3-pre.a.0", "1,2,3,pre,a,N0") - valid("1.2.3-pre.0.a", "1.2.3-pre.0.a", "1,2,3,pre,N0,a") - valid("1.2.3-pre.a.10", "1.2.3-pre.a.10", "1,2,3,pre,a,N10") - valid("1.2.3-pre.a.10", "1.2.3-pre.a.10", "1,2,3,pre,a,N10") - invalid("1.2.3-pre.a.01") - invalid("1.2.3-pre.a..1") - invalid("1.2.3-pre.a.01.1") - invalid("1.2.3-pre.a.01*.1") - - valid("1.2.3+build3", "1.2.3+build3", "1,2,3;build3") - invalid("1.2.3+build3+build2") - valid("1.2.3+build3.123.001", "1.2.3+build3.123.001", "1,2,3;build3;123;001") - invalid("1.2.3+build3.123..001") - invalid("1.2.3+build3.123*.001") - valid("1.2.3-0+build3", "1.2.3-0+build3", "1,2,3,N0;build3") - valid("1.2.3-pre+build3", "1.2.3-pre+build3", "1,2,3,pre;build3") - valid("1.2.3-pre.a+build3", "1.2.3-pre.a+build3", "1,2,3,pre,a;build3") - valid("1.2.3-pre.a.0+build3", "1.2.3-pre.a.0+build3", "1,2,3,pre,a,N0;build3") - valid("1.2.3-pre.0.a+build3", "1.2.3-pre.0.a+build3", "1,2,3,pre,N0,a;build3") - valid("1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3", "1,2,3,pre,a,N10;build3") - valid("1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3", "1,2,3,pre,a,N10;build3") - invalid("1.2.3-pre.a.01+build3") - invalid("1.2.3-pre.a..1+build3") - invalid("1.2.3-pre.a.01.1+build3") - invalid("1.2.3-pre.a.01*.1+build3") - valid("1.2.3-0+build3.123.001", "1.2.3-0+build3.123.001", "1,2,3,N0;build3;123;001") - valid("1.2.3-pre+build3.123.001", "1.2.3-pre+build3.123.001", "1,2,3,pre;build3;123;001") - valid("1.2.3-pre.a+build3.123.001", "1.2.3-pre.a+build3.123.001", "1,2,3,pre,a;build3;123;001") - valid("1.2.3-pre.a.10+build3.123.001", "1.2.3-pre.a.10+build3.123.001", "1,2,3,pre,a,N10;build3;123;001") - invalid("1.2.3-pre.a.+build3.123.001") - invalid("1.2.3-pre.a.01+build3.123.001") - invalid("1.2.3-pre.a.01*+build3.123.001") - - valid("1.2.3-pre.a-10.20.c-30", "1.2.3-pre.a-10.20.c-30", "1,2,3,pre,a-10,N20,c-30") - valid("1.2.3--1-.23.1", "1.2.3--1-.23.1", "1,2,3,-1-,N23,N1") - - invalid("1.2.3.4") - invalid("1.2.3.") + t.Run("NoMajorOrSingleMajorVariants", func(t *testing.T) { + valid("", "0.0.0", ",,,,,") + invalid("0.0.0.0") + invalid("a") + invalid(".") + invalid("-ab") + invalid("+ab") + valid("0", "0.0.0", "0,0,,,,") + valid("0.0.0", "0.0.0", "0.0.0,0,0,0,,") + valid("1", "1.0.0", "1,1,,,,") + valid("1.0.0", "1.0.0", "1.0.0,1,0,0,,") + valid("14", "14.0.0", "14,14,,,,") + valid("123456789123456789123456789", "123456789123456789123456789.0.0", "123456789123456789123456789,123456789123456789123456789,,,,") + invalid("12ab") + invalid("01") + invalid("0ab") + invalid(".1.1") + invalid("1-") + valid("1-0", "1.0.0-0", "1-0,1,,,0,") + valid("1-pre", "1.0.0-pre", "1-pre,1,,,pre,") + valid("1-pre.a", "1.0.0-pre.a", "1-pre.a,1,,,pre.a,") + valid("1-pre.a.0", "1.0.0-pre.a.0", "1-pre.a.0,1,,,pre.a.0,") + valid("1-pre.0.a", "1.0.0-pre.0.a", "1-pre.0.a,1,,,pre.0.a,") + valid("1-pre.a.10", "1.0.0-pre.a.10", "1-pre.a.10,1,,,pre.a.10,") + invalid("1-pre.a.01") + invalid("1-pre.a..1") + invalid("1-pre.a.01.1") + invalid("1-pre.a.01*.1") + valid("1+build3", "1.0.0+build3", "1+build3,1,,,,build3") + invalid("1+build3+build2") + valid("1+build3.123.001", "1.0.0+build3.123.001", "1+build3.123.001,1,,,,build3.123.001") + invalid("1+build3.123..001") + invalid("1+build3.123*.001") + valid("1-0+build3", "1.0.0-0+build3", "1-0+build3,1,,,0,build3") + valid("1-pre+build3", "1.0.0-pre+build3", "1-pre+build3,1,,,pre,build3") + valid("1-pre.a+build3", "1.0.0-pre.a+build3", "1-pre.a+build3,1,,,pre.a,build3") + valid("1-pre.a.10+build3", "1.0.0-pre.a.10+build3", "1-pre.a.10+build3,1,,,pre.a.10,build3") + invalid("1-pre.a.01+build3") + invalid("1-pre.a..1+build3") + invalid("1-pre.a.01.1+build3") + invalid("1-pre.a.01*.1+build3") + valid("1-0+build3.123.001", "1.0.0-0+build3.123.001", "1-0+build3.123.001,1,,,0,build3.123.001") + valid("1-pre+build3.123.001", "1.0.0-pre+build3.123.001", "1-pre+build3.123.001,1,,,pre,build3.123.001") + valid("1-pre.a+build3.123.001", "1.0.0-pre.a+build3.123.001", "1-pre.a+build3.123.001,1,,,pre.a,build3.123.001") + valid("1-pre.a.0+build3.123.001", "1.0.0-pre.a.0+build3.123.001", "1-pre.a.0+build3.123.001,1,,,pre.a.0,build3.123.001") + valid("1-pre.0.a+build3.123.001", "1.0.0-pre.0.a+build3.123.001", "1-pre.0.a+build3.123.001,1,,,pre.0.a,build3.123.001") + valid("1-pre.a.10+build3.123.001", "1.0.0-pre.a.10+build3.123.001", "1-pre.a.10+build3.123.001,1,,,pre.a.10,build3.123.001") + invalid("1-pre.a.+build3.123.001") + invalid("1-pre.a.01+build3.123.001") + invalid("1-pre.a.01*+build3.123.001") + }) + + t.Run("NoMinorOrSingleMinorVariants", func(t *testing.T) { + invalid("1.") + invalid("1.a") + invalid("1..2") + valid("1.2", "1.2.0", "1.2,1,2,,,") + valid("1.0", "1.0.0", "1.0,1,0,,,") + invalid("1.02") + invalid("1.0ab") + invalid("1.12ab") + valid("1.123456789123456789123456789", "1.123456789123456789123456789.0", "1.123456789123456789123456789,1,123456789123456789123456789,,,") + invalid("1.2-") + valid("1.2-0", "1.2.0-0", "1.2-0,1,2,,0,") + valid("1.2-pre", "1.2.0-pre", "1.2-pre,1,2,,pre,") + valid("1.2-pre.a", "1.2.0-pre.a", "1.2-pre.a,1,2,,pre.a,") + valid("1.2-pre.a.0", "1.2.0-pre.a.0", "1.2-pre.a.0,1,2,,pre.a.0,") + valid("1.2-pre.0.a", "1.2.0-pre.0.a", "1.2-pre.0.a,1,2,,pre.0.a,") + valid("1.2-pre.a.10", "1.2.0-pre.a.10", "1.2-pre.a.10,1,2,,pre.a.10,") + invalid("1.2-pre.a.01") + invalid("1.2-pre.a..1") + invalid("1.2-pre.a.01.1") + invalid("1.2-pre.a.01*.1") + + valid("1.2+build3", "1.2.0+build3", "1.2+build3,1,2,,,build3") + valid("1.2-0+build3", "1.2.0-0+build3", "1.2-0+build3,1,2,,0,build3") + invalid("1.2+build3+build2") + valid("1.2+build3.123.001", "1.2.0+build3.123.001", "1.2+build3.123.001,1,2,,,build3.123.001") + invalid("1.2+build3.123..001") + invalid("1.2+build3.123*.001") + valid("1.2-pre+build3", "1.2.0-pre+build3", "1.2-pre+build3,1,2,,pre,build3") + valid("1.2-pre.a.0+build3", "1.2.0-pre.a.0+build3", "1.2-pre.a.0+build3,1,2,,pre.a.0,build3") + valid("1.2-pre.0.a+build3", "1.2.0-pre.0.a+build3", "1.2-pre.0.a+build3,1,2,,pre.0.a,build3") + valid("1.2-pre.a.10+build3", "1.2.0-pre.a.10+build3", "1.2-pre.a.10+build3,1,2,,pre.a.10,build3") + valid("1.2-pre.a+build3", "1.2.0-pre.a+build3", "1.2-pre.a+build3,1,2,,pre.a,build3") + valid("1.2-pre.a.10+build3", "1.2.0-pre.a.10+build3", "1.2-pre.a.10+build3,1,2,,pre.a.10,build3") + invalid("1.2-pre.a.01+build3") + invalid("1.2-pre.a..1+build3") + invalid("1.2-pre.a.01.1+build3") + invalid("1.2-pre.a.01*.1+build3") + valid("1.2-0+build3.123.001", "1.2.0-0+build3.123.001", "1.2-0+build3.123.001,1,2,,0,build3.123.001") + valid("1.2-pre+build3.123.001", "1.2.0-pre+build3.123.001", "1.2-pre+build3.123.001,1,2,,pre,build3.123.001") + valid("1.2-pre.a+build3.123.001", "1.2.0-pre.a+build3.123.001", "1.2-pre.a+build3.123.001,1,2,,pre.a,build3.123.001") + valid("1.2-pre.a.0+build3.123.001", "1.2.0-pre.a.0+build3.123.001", "1.2-pre.a.0+build3.123.001,1,2,,pre.a.0,build3.123.001") + valid("1.2-pre.0.a+build3.123.001", "1.2.0-pre.0.a+build3.123.001", "1.2-pre.0.a+build3.123.001,1,2,,pre.0.a,build3.123.001") + valid("1.2-pre.a.10+build3.123.001", "1.2.0-pre.a.10+build3.123.001", "1.2-pre.a.10+build3.123.001,1,2,,pre.a.10,build3.123.001") + valid("1.2-pre.a.10+build3.123.001", "1.2.0-pre.a.10+build3.123.001", "1.2-pre.a.10+build3.123.001,1,2,,pre.a.10,build3.123.001") + invalid("1.2-pre.a.+build3.123.001") + invalid("1.2-pre.a.01+build3.123.001") + invalid("1.2-pre.a.01*+build3.123.001") + }) + + t.Run("FullVariants", func(t *testing.T) { + invalid("1.2.a") + invalid("1.2.") + valid("1.2.3", "1.2.3", "1.2.3,1,2,3,,") + valid("1.2.0", "1.2.0", "1.2.0,1,2,0,,") + invalid("1.2.03") + invalid("1.2.0ab") + invalid("1.2.34ab") + valid("1.2.123456789123456789123456789", "1.2.123456789123456789123456789", "1.2.123456789123456789123456789,1,2,123456789123456789123456789,,") + invalid("1.2.3-") + valid("1.2.3-0", "1.2.3-0", "1.2.3-0,1,2,3,0,") + valid("1.2.3-pre", "1.2.3-pre", "1.2.3-pre,1,2,3,pre,") + valid("1.2.3-pre.a", "1.2.3-pre.a", "1.2.3-pre.a,1,2,3,pre.a,") + valid("1.2.3-pre.a.0", "1.2.3-pre.a.0", "1.2.3-pre.a.0,1,2,3,pre.a.0,") + valid("1.2.3-pre.0.a", "1.2.3-pre.0.a", "1.2.3-pre.0.a,1,2,3,pre.0.a,") + valid("1.2.3-pre.a.10", "1.2.3-pre.a.10", "1.2.3-pre.a.10,1,2,3,pre.a.10,") + valid("1.2.3-pre.a.10", "1.2.3-pre.a.10", "1.2.3-pre.a.10,1,2,3,pre.a.10,") + invalid("1.2.3-pre.a.01") + invalid("1.2.3-pre.a..1") + invalid("1.2.3-pre.a.01.1") + invalid("1.2.3-pre.a.01*.1") + + valid("1.2.3+build3", "1.2.3+build3", "1.2.3+build3,1,2,3,,build3") + invalid("1.2.3+build3+build2") + valid("1.2.3+build3.123.001", "1.2.3+build3.123.001", "1.2.3+build3.123.001,1,2,3,,build3.123.001") + invalid("1.2.3+build3.123..001") + invalid("1.2.3+build3.123*.001") + valid("1.2.3-0+build3", "1.2.3-0+build3", "1.2.3-0+build3,1,2,3,0,build3") + valid("1.2.3-pre+build3", "1.2.3-pre+build3", "1.2.3-pre+build3,1,2,3,pre,build3") + valid("1.2.3-pre.a+build3", "1.2.3-pre.a+build3", "1.2.3-pre.a+build3,1,2,3,pre.a,build3") + valid("1.2.3-pre.a.0+build3", "1.2.3-pre.a.0+build3", "1.2.3-pre.a.0+build3,1,2,3,pre.a.0,build3") + valid("1.2.3-pre.0.a+build3", "1.2.3-pre.0.a+build3", "1.2.3-pre.0.a+build3,1,2,3,pre.0.a,build3") + valid("1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3,1,2,3,pre.a.10,build3") + valid("1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3,1,2,3,pre.a.10,build3") + invalid("1.2.3-pre.a.01+build3") + invalid("1.2.3-pre.a..1+build3") + invalid("1.2.3-pre.a.01.1+build3") + invalid("1.2.3-pre.a.01*.1+build3") + valid("1.2.3-0+build3.123.001", "1.2.3-0+build3.123.001", "1.2.3-0+build3.123.001,1,2,3,0,build3.123.001") + valid("1.2.3-pre+build3.123.001", "1.2.3-pre+build3.123.001", "1.2.3-pre+build3.123.001,1,2,3,pre,build3.123.001") + valid("1.2.3-pre.a+build3.123.001", "1.2.3-pre.a+build3.123.001", "1.2.3-pre.a+build3.123.001,1,2,3,pre.a,build3.123.001") + valid("1.2.3-pre.a.10+build3.123.001", "1.2.3-pre.a.10+build3.123.001", "1.2.3-pre.a.10+build3.123.001,1,2,3,pre.a.10,build3.123.001") + invalid("1.2.3-pre.a.+build3.123.001") + invalid("1.2.3-pre.a.01+build3.123.001") + invalid("1.2.3-pre.a.01*+build3.123.001") + + invalid("1.2.3.4") + invalid("1.2.3.") + }) + + t.Run("AbsurdlyWeirdVersions", func(t *testing.T) { + valid("1.2.3-pre.a-10.20.c-30", "1.2.3-pre.a-10.20.c-30", "1.2.3-pre.a-10.20.c-30,1,2,3,pre.a-10.20.c-30,") + valid("1.2.3--1-.23.1", "1.2.3--1-.23.1", "1.2.3--1-.23.1,1,2,3,-1-.23.1,") + }) } func TestNilVersionStringOutput(t *testing.T) { diff --git a/version.go b/version.go index 1d88da7..b4ee5c6 100644 --- a/version.go +++ b/version.go @@ -8,42 +8,20 @@ package semver // Version contains the results of parsed version string type Version struct { - major []byte - minor []byte - patch []byte - prerelases [][]byte - numericPrereleases []bool - builds [][]byte + raw string + bytes []byte + major int + minor int + patch int + prerelease int + build int } func (v *Version) String() string { if v == nil { return "" } - res := string(v.major) - if len(v.minor) > 0 { - res += "." + string(v.minor) - } - if len(v.patch) > 0 { - res += "." + string(v.patch) - } - for i, prerelease := range v.prerelases { - if i == 0 { - res += "-" - } else { - res += "." - } - res += string(prerelease) - } - for i, build := range v.builds { - if i == 0 { - res += "+" - } else { - res += "." - } - res += string(build) - } - return res + return v.raw } // NormalizedString is a datatype to be used in maps and other places where the @@ -57,78 +35,54 @@ func (v *Version) NormalizedString() NormalizedString { if v == nil { return "" } - res := NormalizedString("") - if len(v.major) > 0 { - res += NormalizedString(v.major) - } else { - res += "0" - } - - if len(v.minor) > 0 { - res += "." + NormalizedString(v.minor) - } else { - res += ".0" - } - if len(v.patch) > 0 { - res += "." + NormalizedString(v.patch) + if v.major == 0 { + return NormalizedString("0.0.0") + } else if v.minor == v.major { + return NormalizedString(v.raw[0:v.major] + ".0.0" + v.raw[v.major:]) + } else if v.patch == v.minor { + return NormalizedString(v.raw[0:v.minor] + ".0" + v.raw[v.minor:]) } else { - res += ".0" - } - for i, prerelease := range v.prerelases { - if i == 0 { - res += "-" - } else { - res += "." - } - res += NormalizedString(prerelease) - } - for i, build := range v.builds { - if i == 0 { - res += "+" - } else { - res += "." - } - res += NormalizedString(build) + return NormalizedString(v.raw) } - return res } -var zero = []byte("0") - // Normalize transforms a truncated semver version in a strictly compliant semver // version by adding minor and patch versions. For example: // "1" is trasformed to "1.0.0" or "2.5-dev" to "2.5.0-dev" func (v *Version) Normalize() { - if len(v.major) == 0 { - v.major = zero[0:1] - } - if len(v.minor) == 0 { - v.minor = zero[0:1] - } - if len(v.patch) == 0 { - v.patch = zero[0:1] + if v.major == 0 { + v.raw = "0.0.0" + v.raw + v.major = 1 + v.minor = 3 + v.patch = 5 + v.prerelease += 5 + v.build += 5 + } else if v.minor == v.major { + v.raw = v.raw[0:v.major] + ".0.0" + v.raw[v.major:] + v.minor = v.major + 2 + v.patch = v.major + 4 + v.prerelease += 4 + v.build += 4 + } else if v.patch == v.minor { + v.raw = v.raw[0:v.minor] + ".0" + v.raw[v.minor:] + v.patch = v.minor + 2 + v.prerelease += 2 + v.build += 2 } } func compareNumber(a, b []byte) int { la := len(a) - if la == 0 { - a = zero[:] - la = 1 - } lb := len(b) - if lb == 0 { - b = zero[:] - lb = 1 - } if la == lb { for i := range a { + if a[i] == b[i] { + continue + } if a[i] > b[i] { return 1 } - if a[i] < b[i] { - return -1 - } + return -1 } return 0 } @@ -148,42 +102,120 @@ func compareAlpha(a, b []byte) int { return 0 } +var zero = []byte("0") + // CompareTo compares the Version with the one passed as parameter. // Returns -1, 0 or 1 if the version is respectively less than, equal // or greater than the compared Version func (v *Version) CompareTo(u *Version) int { // 11. Precedence refers to how versions are compared to each other when ordered. - // Precedence MUST be calculated by separating the version into major, minor, + // Precedence MUST be calculated by separating the version into cmp, minor, // patch and pre-release identifiers in that order (Build metadata does not // figure into precedence). Precedence is determined by the first difference when // comparing each of these identifiers from left to right as follows: Major, minor, // and patch versions are always compared numerically. // Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. - major := compareNumber(v.major, u.major) - if major != 0 { - return major + vIdx := 0 + uIdx := 0 + vMajor := v.major + uMajor := u.major + { + if vMajor == uMajor { + for vIdx < vMajor { + if v.bytes[vIdx] == u.bytes[uIdx] { + vIdx++ + uIdx++ + continue + } + if v.bytes[vIdx] > u.bytes[uIdx] { + return 1 + } + return -1 + } + } else if vMajor == 0 && u.bytes[uIdx] == '0' { + return 0 + } else if uMajor == 0 && v.bytes[vIdx] == '0' { + return 0 + } else if vMajor > uMajor { + return 1 + } else { + return -1 + } } - minor := compareNumber(v.minor, u.minor) - if minor != 0 { - return minor + vMinor := v.minor + uMinor := u.minor + vIdx = vMajor + 1 + uIdx = uMajor + 1 + { + la := vMinor - vMajor - 1 + lb := uMinor - uMajor - 1 + if la == lb { + for vIdx < vMinor { + if v.bytes[vIdx] == u.bytes[uIdx] { + vIdx++ + uIdx++ + continue + } + if v.bytes[vIdx] > u.bytes[uIdx] { + return 1 + } + return -1 + } + } else if vMinor == vMajor && u.bytes[uIdx] == '0' { + return 0 + } else if uMinor == uMajor && v.bytes[vIdx] == '0' { + return 0 + } else if la > lb { + return 1 + } else { + return -1 + } } - patch := compareNumber(v.patch, u.patch) - if patch != 0 { - return patch + vPatch := v.patch + uPatch := u.patch + vIdx = vMinor + 1 + uIdx = uMinor + 1 + { + la := vPatch - vMinor - 1 + lb := uPatch - uMinor - 1 + if la == lb { + for vIdx < vPatch { + if v.bytes[vIdx] == u.bytes[uIdx] { + vIdx++ + uIdx++ + continue + } + if v.bytes[vIdx] > u.bytes[uIdx] { + return 1 + } + return -1 + } + } else if vPatch == vMinor && u.bytes[uIdx] == '0' { + return 0 + } else if uPatch == uMinor && v.bytes[vIdx] == '0' { + return 0 + } else if la > lb { + return 1 + } else { + return -1 + } + } + + // if both versions have no pre-release, they are equal + if v.prerelease == vPatch && u.prerelease == uPatch { + return 0 } // When major, minor, and patch are equal, a pre-release version has lower // precedence than a normal version. // Example: 1.0.0-alpha < 1.0.0. - lv := len(v.prerelases) - lu := len(u.prerelases) - if lv == 0 && lu == 0 { - return 0 - } - if lv == 0 { + + // if v has no pre-release, it's greater than u + if v.prerelease == vPatch { return 1 } - if lu == 0 { + // if u has no pre-release, it's greater than v + if u.prerelease == uPatch { return -1 } @@ -197,35 +229,120 @@ func (v *Version) CompareTo(u *Version) int { // if all of the preceding identifiers are equal. // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < // < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. - min := lv - if lv > lu { - min = lu - } - for i := 0; i < min; i++ { - if v.numericPrereleases[i] && u.numericPrereleases[i] { - comp := compareNumber(v.prerelases[i], u.prerelases[i]) - if comp != 0 { - return comp + vIdx = vPatch + 1 + uIdx = uPatch + 1 + vLast := v.prerelease + uLast := u.prerelease + vIsAlpha := false + uIsAlpha := false + vIsLonger := false + uIsLonger := false + cmp := 0 + for { + var vCurr byte + var uCurr byte + if vIdx != vLast { + vCurr = v.raw[vIdx] + } + if uIdx != uLast { + uCurr = u.raw[uIdx] + } + + if vIdx == vLast || vCurr == '.' { + if uIdx != uLast && uCurr != '.' { + if !uIsAlpha && !(uCurr >= '0' && uCurr <= '9') { + uIsAlpha = true + } + uIsLonger = true + uIdx++ + continue + } + } else if uIdx == uLast || uCurr == '.' { + if vIdx != vLast && vCurr != '.' { + if !vIsAlpha && !(vCurr >= '0' && vCurr <= '9') { + vIsAlpha = true + } + vIsLonger = true + vIdx++ + continue } + } else { + if cmp == 0 { + if vCurr > uCurr { + cmp = 1 + } else if vCurr < uCurr { + cmp = -1 + } + } + if !vIsAlpha && !(vCurr >= '0' && vCurr <= '9') { + vIsAlpha = true + } + if !uIsAlpha && !(uCurr >= '0' && uCurr <= '9') { + uIsAlpha = true + } + vIdx++ + uIdx++ continue } - if v.numericPrereleases[i] { + + // Numeric identifiers always have lower precedence than non-numeric identifiers. + if vIsAlpha && uIsAlpha { + if cmp != 0 { + // alphanumeric vs alphanumeric, sorting has priority + return cmp + } else if vIsLonger { + // alphanumeric vs alphanumeric, v is longer, return > + return 1 + } else if uIsLonger { + // alphanumeric vs alphanumeric, u is longer, return < + return -1 + } + // Both alphanumeric, if comparison is equal, move on the next field + } else if vIsAlpha && !uIsAlpha { + // alphanumeric vs numeric, return > + return 1 + } else if !vIsAlpha && uIsAlpha { + // numeric vs alphanumeric, return < return -1 + } else { + if vIsLonger { + // numeric vs numeric, v is longer, return > + return 1 + } else if uIsLonger { + // numeric vs numeric, u is longer, return < + return -1 + } else if cmp != 0 { + // numeric vs numeric, return cmp if not equal + return cmp + } + // Both numeric, if comparison is equal, move on the next field + } + + // A larger set of pre-release fields has a higher precedence than a smaller set, + // if all of the preceding identifiers are equal. + + if vIdx == vLast && uIdx == uLast { + // No more field, proceed with build metadata + break } - if u.numericPrereleases[i] { + if vIdx != vLast && uIdx == uLast { + // v has more fields, return > return 1 } - comp := compareAlpha(v.prerelases[i], u.prerelases[i]) - if comp != 0 { - return comp + if vIdx == vLast && uIdx != uLast { + // u has more fields, return < + return -1 } + + // Move on the next field + vIsAlpha = false + uIsAlpha = false + vIsLonger = false + uIsLonger = false + vIdx++ + uIdx++ } - if lv > lu { - return 1 - } - if lv < lu { - return -1 - } + return 0 } @@ -259,11 +376,43 @@ func (v *Version) CompatibleWith(u *Version) bool { if !u.GreaterThanOrEqual(v) { return false } - if v.major[0] != '0' { - return compareNumber(u.major, v.major) == 0 - } else if v.minor[0] != '0' { - return compareNumber(u.major, v.major) == 0 && compareNumber(u.minor, v.minor) == 0 - } else { - return compareNumber(u.major, v.major) == 0 && compareNumber(u.minor, v.minor) == 0 && compareNumber(u.patch, v.patch) == 0 + vMajor := zero[:] + if v.major > 0 { + vMajor = v.bytes[:v.major] + } + uMajor := zero[:] + if u.major > 0 { + uMajor = u.bytes[:u.major] + } + majorEquals := compareNumber(vMajor, uMajor) == 0 + if v.major > 0 && v.bytes[0] != '0' { + return majorEquals + } + if !majorEquals { + return false + } + vMinor := zero[:] + if v.minor > v.major { + vMinor = v.bytes[v.major+1 : v.minor] + } + uMinor := zero[:] + if u.minor > u.major { + uMinor = u.bytes[u.major+1 : u.minor] + } + minorEquals := compareNumber(vMinor, uMinor) == 0 + if vMinor[0] != '0' { + return minorEquals + } + if !minorEquals { + return false + } + vPatch := zero[:] + if v.patch > v.minor { + vPatch = v.bytes[v.minor+1 : v.patch] + } + uPatch := zero[:] + if u.patch > u.minor { + uPatch = u.bytes[u.minor+1 : u.patch] } + return compareNumber(vPatch, uPatch) == 0 } diff --git a/version_test.go b/version_test.go index 104c089..6c54e3a 100644 --- a/version_test.go +++ b/version_test.go @@ -24,13 +24,14 @@ func ascending(t *testing.T, allowEqual bool, list ...string) { a := MustParse(list[i]) b := MustParse(list[i+1]) comp := a.CompareTo(b) - fmt.Printf("%s %s %s\n", a, sign[comp], b) if allowEqual { + fmt.Printf("%s %s= %s\n", list[i], sign[comp], list[i+1]) require.LessOrEqual(t, comp, 0) require.True(t, a.LessThanOrEqual(b)) require.False(t, a.GreaterThan(b)) } else { - require.Equal(t, comp, -1) + fmt.Printf("%s %s %s\n", list[i], sign[comp], list[i+1]) + require.Equal(t, comp, -1, "cmp(%s, %s) must return '<', but returned '%s'", list[i], list[i+1], sign[comp]) require.True(t, a.LessThan(b)) require.True(t, a.LessThanOrEqual(b)) require.False(t, a.Equal(b)) @@ -41,7 +42,7 @@ func ascending(t *testing.T, allowEqual bool, list ...string) { comp = b.CompareTo(a) fmt.Printf("%s %s %s\n", b, sign[comp], a) if allowEqual { - require.GreaterOrEqual(t, comp, 0) + require.GreaterOrEqual(t, comp, 0, "cmp(%s, %s) must return '>=', but returned '%s'", b, a, sign[comp]) require.False(t, b.LessThan(a)) require.True(t, b.GreaterThanOrEqual(a)) } else { @@ -61,38 +62,56 @@ func TestVersionComparator(t *testing.T) { for _, b := range list[i+1:] { comp := a.CompareTo(b) fmt.Printf("%s %s %s\n", a, sign[comp], b) - require.Equal(t, comp, 0) - require.False(t, a.LessThan(b)) - require.True(t, a.LessThanOrEqual(b)) - require.True(t, a.Equal(b)) - require.True(t, a.GreaterThanOrEqual(b)) - require.False(t, a.GreaterThan(b)) + require.Equal(t, comp, 0, "cmp(%s, %s) must return '=', but returned '%s'", a, b, sign[comp]) + require.False(t, a.LessThan(b), "NOT wanted: %s < %s", a, b) + require.True(t, a.LessThanOrEqual(b), "wanted: %s <= %s", a, b) + require.True(t, a.Equal(b), "wanted: %s = %s", a, b) + require.True(t, a.GreaterThanOrEqual(b), "wanted: %s >= %s", a, b) + require.False(t, a.GreaterThan(b), "NOT wanted: %s > %s", a, b) comp = b.CompareTo(a) fmt.Printf("%s %s %s\n", b, sign[comp], a) - require.Equal(t, comp, 0) - require.False(t, b.LessThan(a)) - require.True(t, b.LessThanOrEqual(a)) - require.True(t, b.Equal(a)) - require.True(t, b.GreaterThanOrEqual(a)) - require.False(t, b.GreaterThan(a)) + require.Equal(t, comp, 0, "cmp(%s, %s) must return '=', but returned '%s'", b, a, sign[comp]) + require.False(t, b.LessThan(a), "NOT wanted: %s < %s", b, a) + require.True(t, b.LessThanOrEqual(a), "wanted: %s <= %s", b, a) + require.True(t, b.Equal(a), "wanted: %s = %s", b, a) + require.True(t, b.GreaterThanOrEqual(a), "wanted: %s >= %s", b, a) + require.False(t, b.GreaterThan(a), "NOT wanted: %s > %s", b, a) } } } ascending(t, false, + "1.0.0-2", + "1.0.0-11", + "1.0.0-11a", "1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-alpha.beta", "1.0.0-beta", "1.0.0-beta.2", "1.0.0-beta.11", + "1.0.0-beta.11a", "1.0.0-rc.1", "1.0.0", "1.0.1", "1.1.1", + "1.1.8", + "1.1.22", "1.6.22", "1.8.1", + "1.20.0", "2.1.1", + "10.0.0", + "17.3.0-atmel3.6.1-arduino7", + "17.3.0-atmel3.6.1-arduino7not", + "17.3.0-atmel3.6.1-beduino8", + "17.3.0-atmel3.6.1-beduino8not", + "17.3.0-atmel3a.6.1-arduino7", + "17.3.0-atmel3a.16.2.arduino7", + "17.3.0-atmel3a.16.12.arduino7", + "17.3.0-atmel3a.16.1-arduino7", + "17.3.0-atmel3a.16.12-arduino7", + "17.3.0-atmel3a.16.2-arduino7", ) equal( MustParse(""), @@ -213,3 +232,24 @@ func TestNilVersionString(t *testing.T) { var nilVersion *Version require.Equal(t, "", nilVersion.String()) } + +func TestCompareNumbers(t *testing.T) { + // == + require.Zero(t, compareNumber([]byte("0"), []byte("0"))) + require.Zero(t, compareNumber([]byte("5"), []byte("5"))) + require.Zero(t, compareNumber([]byte("15"), []byte("15"))) + + // > + testGreater := func(a, b string) { + require.Positive(t, compareNumber([]byte(a), []byte(b)), `compareNumber("%s","%s") is not positive`, a, b) + require.Negative(t, compareNumber([]byte(b), []byte(a)), `compareNumber("%s","%s") is not negative`, b, a) + } + testGreater("1", "") + testGreater("1", "0") + testGreater("1", "") + testGreater("2", "1") + testGreater("10", "") + testGreater("10", "0") + testGreater("10", "1") + testGreater("10", "2") +} diff --git a/yaml.go b/yaml.go index d8525e4..fc8e942 100644 --- a/yaml.go +++ b/yaml.go @@ -26,12 +26,13 @@ func (v *Version) UnmarshalYAML(node *yaml.Node) error { return err } + v.raw = parsed.raw + v.bytes = []byte(v.raw) v.major = parsed.major v.minor = parsed.minor v.patch = parsed.patch - v.prerelases = parsed.prerelases - v.numericPrereleases = parsed.numericPrereleases - v.builds = parsed.builds + v.prerelease = parsed.prerelease + v.build = parsed.build return nil } diff --git a/yaml_test.go b/yaml_test.go index 19f576d..255c617 100644 --- a/yaml_test.go +++ b/yaml_test.go @@ -32,11 +32,8 @@ func TestYAMLParseVersion(t *testing.T) { err = yaml.Unmarshal(data, &u) require.NoError(t, err) - dump := fmt.Sprintf("%s,%s,%s,%s,%v,%s", - u.major, u.minor, u.patch, - u.prerelases, u.numericPrereleases, - u.builds) - require.Equal(t, "1,2,3,[aaa 4 5 6],[false true true true],[bbb 7 8 9]", dump) + dump := fmt.Sprintf("%v,%v,%v,%v,%v,%v", u.raw, u.major, u.minor, u.patch, u.prerelease, u.build) + require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9,1,3,5,15,25", dump) require.Equal(t, testVersion, u.String()) @@ -45,6 +42,10 @@ func TestYAMLParseVersion(t *testing.T) { err = yaml.Unmarshal([]byte(`invalid:`), &u) require.Error(t, err) + + require.NoError(t, yaml.Unmarshal([]byte(`"1.6.2"`), &v)) + require.NoError(t, yaml.Unmarshal([]byte(`"1.6.3"`), &u)) + require.True(t, u.GreaterThan(v)) } func TestYAMLParseRelaxedVersion(t *testing.T) {