commit 749af4bbe86768ebb907ffacbb2c127f34014a07 Author: Pavel Merzlyakov Date: Sat Oct 1 17:48:52 2022 +0300 initial commit diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3393088 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module git.bit5.ru/backend/versioning + +go 1.18 + +require ( + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.8.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8bde76e --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/versioning.go b/versioning.go new file mode 100644 index 0000000..242ede6 --- /dev/null +++ b/versioning.go @@ -0,0 +1,144 @@ +package versioning + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +const ( + MaxMajor uint16 = 42948 + MaxMinor uint16 = 999 + MaxPatch uint8 = 99 +) + +var maxVersion = newVersion(MaxMajor, MaxMinor, MaxPatch) + +type Layout uint8 + +const ( + Full Layout = iota + WithoutPatch +) + +type Version struct { + major uint16 + minor uint16 + patch uint8 + code uint32 +} + +func (v Version) String() string { + return v.Format(Full) +} + +func (v Version) Format(l Layout) string { + switch l { + default: + return fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch) + + case WithoutPatch: + return fmt.Sprintf("%d.%d", v.major, v.minor) + } +} + +func (v Version) Code() uint32 { + return v.code +} + +func (a Version) Less(b Version) bool { + return a.Code() < b.Code() +} + +func Parse(str string) (Version, error) { + parts := strings.Split(str, ".") + partsLen := len(parts) + if partsLen > 3 { + return Version{}, errors.Errorf("invalid game version: '%s'", str) + } + + maj, err := parseUint16(parts[0]) + if err != nil { + return Version{}, err + } + + var ( + min uint16 + patch uint8 + ) + if partsLen > 1 { + min, err = parseUint16(parts[1]) + if err != nil { + return Version{}, err + } + } + if partsLen > 2 { + patch, err = parseUint8(parts[2]) + if err != nil { + return Version{}, err + } + } + + if err := validate(maj, min, patch); err != nil { + return Version{}, err + } + return newVersion(maj, min, patch), nil +} + +func ParseFromCode(code uint32) (Version, error) { + patch := code % 100 + minor := code%100000 - patch + major := code - minor - patch + + maj, min, p := uint16(major/100000), uint16(minor/100), uint8(patch) + + if err := validate(maj, min, p); err != nil { + return Version{}, err + } + + return Version{ + major: maj, + minor: min, + patch: p, + code: code, + }, nil +} + +func parseUint16(str string) (uint16, error) { + v, err := strconv.ParseUint(str, 10, 16) + if err != nil { + return 0, errors.WithStack(err) + } + return uint16(v), nil +} + +func parseUint8(str string) (uint8, error) { + v, err := strconv.ParseUint(str, 10, 8) + if err != nil { + return 0, errors.WithStack(err) + } + return uint8(v), nil +} + +func validate(major, minor uint16, patch uint8) error { + if major > MaxMajor || minor > MaxMinor || patch > MaxPatch { + return errors.Errorf("invalid game version. max version: %s", maxVersion) + } + + return nil +} + +func newVersion(major, minor uint16, patch uint8) Version { + return Version{ + major: major, + minor: minor, + patch: patch, + code: calcCode(major, minor, patch), + } +} + +func calcCode(major, minor uint16, patch uint8) uint32 { + return uint32(major)*100000 + uint32(minor)*100 + uint32(patch) +} diff --git a/versioning_test.go b/versioning_test.go new file mode 100644 index 0000000..99fa537 --- /dev/null +++ b/versioning_test.go @@ -0,0 +1,134 @@ +package versioning_test + +import ( + "testing" + + "git.bit5.ru/backend/versioning" + + "github.com/stretchr/testify/require" +) + +func TestParse(t *testing.T) { + cases := []struct { + versionStr string + valid bool + expectedStr string + expectedCode uint32 + }{ + {versionStr: "0.0.0", valid: true, expectedStr: "0.0.0", expectedCode: 0}, + {versionStr: "0.0.1", valid: true, expectedStr: "0.0.1", expectedCode: 1}, + {versionStr: "0.1.0", valid: true, expectedStr: "0.1.0", expectedCode: 100}, + {versionStr: "0.1.1", valid: true, expectedStr: "0.1.1", expectedCode: 101}, + {versionStr: "1.0.0", valid: true, expectedStr: "1.0.0", expectedCode: 100000}, + {versionStr: "1.0.1", valid: true, expectedStr: "1.0.1", expectedCode: 100001}, + {versionStr: "1.1.0", valid: true, expectedStr: "1.1.0", expectedCode: 100100}, + {versionStr: "1.1.1", valid: true, expectedStr: "1.1.1", expectedCode: 100101}, + + {versionStr: "12345.123.12", valid: true, expectedStr: "12345.123.12", expectedCode: 1234512312}, + {versionStr: "42948.999.99", valid: true, expectedStr: "42948.999.99", expectedCode: 4294899999}, + {versionStr: "42948.999.100", valid: false}, + {versionStr: "42948.1000.99", valid: false}, + {versionStr: "42948.1000.100", valid: false}, + {versionStr: "42949.999.99", valid: false}, + {versionStr: "42949.999.100", valid: false}, + {versionStr: "42949.1000.99", valid: false}, + {versionStr: "42949.1000.100", valid: false}, + + {versionStr: "0.0", valid: true, expectedStr: "0.0.0", expectedCode: 0}, + {versionStr: "0.1", valid: true, expectedStr: "0.1.0", expectedCode: 100}, + {versionStr: "1.0", valid: true, expectedStr: "1.0.0", expectedCode: 100000}, + {versionStr: "1.1", valid: true, expectedStr: "1.1.0", expectedCode: 100100}, + + {versionStr: "12345.123", valid: true, expectedStr: "12345.123.0", expectedCode: 1234512300}, + {versionStr: "42948.999", valid: true, expectedStr: "42948.999.0", expectedCode: 4294899900}, + {versionStr: "42948.1000", valid: false}, + {versionStr: "42949.999", valid: false}, + {versionStr: "42949.1000", valid: false}, + + {versionStr: "0", valid: true, expectedStr: "0.0.0", expectedCode: 0}, + {versionStr: "1", valid: true, expectedStr: "1.0.0", expectedCode: 100000}, + + {versionStr: "12345", valid: true, expectedStr: "12345.0.0", expectedCode: 1234500000}, + {versionStr: "42948", valid: true, expectedStr: "42948.0.0", expectedCode: 4294800000}, + {versionStr: "42949", valid: false}, + } + + for i, c := range cases { + caseNum := i + 1 + + v, err := versioning.Parse(c.versionStr) + + if !c.valid { + require.Error(t, err, "case#%d", caseNum) + continue + } + + require.NoError(t, err, "case#%d", caseNum) + require.EqualValues(t, c.expectedStr, v.String(), "case#%d", caseNum) + require.EqualValues(t, c.expectedCode, v.Code(), "case#%d", caseNum) + } +} + +func TestParseFromCode(t *testing.T) { + cases := []struct { + code uint32 + valid bool + expectedStr string + }{ + {code: 0, valid: true, expectedStr: "0.0.0"}, + {code: 1, valid: true, expectedStr: "0.0.1"}, + {code: 10, valid: true, expectedStr: "0.0.10"}, + {code: 99, valid: true, expectedStr: "0.0.99"}, + {code: 100, valid: true, expectedStr: "0.1.0"}, + {code: 101, valid: true, expectedStr: "0.1.1"}, + {code: 110, valid: true, expectedStr: "0.1.10"}, + {code: 199, valid: true, expectedStr: "0.1.99"}, + {code: 99900, valid: true, expectedStr: "0.999.0"}, + {code: 99901, valid: true, expectedStr: "0.999.1"}, + {code: 99910, valid: true, expectedStr: "0.999.10"}, + {code: 99999, valid: true, expectedStr: "0.999.99"}, + {code: 100000, valid: true, expectedStr: "1.0.0"}, + {code: 100001, valid: true, expectedStr: "1.0.1"}, + {code: 100099, valid: true, expectedStr: "1.0.99"}, + {code: 100100, valid: true, expectedStr: "1.1.0"}, + {code: 100101, valid: true, expectedStr: "1.1.1"}, + {code: 100199, valid: true, expectedStr: "1.1.99"}, + {code: 199900, valid: true, expectedStr: "1.999.0"}, + {code: 199901, valid: true, expectedStr: "1.999.1"}, + {code: 199999, valid: true, expectedStr: "1.999.99"}, + {code: 4294800000, valid: true, expectedStr: "42948.0.0"}, + {code: 4294800001, valid: true, expectedStr: "42948.0.1"}, + {code: 4294800099, valid: true, expectedStr: "42948.0.99"}, + {code: 4294800100, valid: true, expectedStr: "42948.1.0"}, + {code: 4294800101, valid: true, expectedStr: "42948.1.1"}, + {code: 4294800199, valid: true, expectedStr: "42948.1.99"}, + {code: 4294899900, valid: true, expectedStr: "42948.999.0"}, + {code: 4294899901, valid: true, expectedStr: "42948.999.1"}, + {code: 4294899999, valid: true, expectedStr: "42948.999.99"}, + {code: 4294900000, valid: false}, + } + + for i, c := range cases { + caseNum := i + 1 + + v, err := versioning.ParseFromCode(c.code) + + if !c.valid { + require.Error(t, err, "case#%d", caseNum) + continue + } + + require.NoError(t, err, "case#%d", caseNum) + require.EqualValues(t, c.expectedStr, v.String(), "case#%d", caseNum) + } +} + +// helper for testing other packages +func Parse(t *testing.T, str string) versioning.Version { + t.Helper() + + v, err := versioning.Parse(str) + require.NoError(t, err) + + return v +}