Skip to content

Instantly share code, notes, and snippets.

@xnyo
Created July 27, 2023 14:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xnyo/a9bed05c0967cba53807ddfe274e5390 to your computer and use it in GitHub Desktop.
Save xnyo/a9bed05c0967cba53807ddfe274e5390 to your computer and use it in GitHub Desktop.
Simple Go semver package
package semver
import (
"errors"
"fmt"
"strconv"
"strings"
)
type SemVer string
func NewFromVersion(major, minor, patch int) SemVer {
return SemVer(fmt.Sprintf("%d.%d.%d", major, minor, patch))
}
func (v SemVer) Split() (major int, minor int, patch int, err error) {
// Remove suffix (e.g.: "10.1.0-pre" -> 10, 1, 0)
parts := strings.SplitN(string(v), "-", 2)
if len(parts) == 0 {
err = errors.New("no parts")
return
}
if len(parts[0]) < 3 {
err = fmt.Errorf("expected at 3 least parts, got %d", len(parts))
return
}
parts = strings.SplitN(parts[0], ".", 3)
// Convert parts to integers and set return values
for i, dst := range []*int{&major, &minor, &patch} {
*dst, err = strconv.Atoi(parts[i])
if err != nil {
err = fmt.Errorf("%q atoi: %w", parts[i], err)
return
}
}
return
}
func (v SemVer) Number() (int, error) {
major, minor, patch, err := v.Split()
if err != nil {
return 0, nil
}
return patch | (minor << 10) | (major << 20), nil
}
func (v SemVer) Compare(cmp SemVer) (int, error) {
vn, err := v.Number()
if err != nil {
return 0, err
}
cmpN, err := cmp.Number()
if err != nil {
return 0, err
}
return vn - cmpN, nil
}
package semver
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSemVer(t *testing.T) {
t.Run("NewFromVersion", func(t *testing.T) {
require.Equal(t, SemVer("1.2.3"), NewFromVersion(1, 2, 3))
})
}
func TestSemVer_Split(t *testing.T) {
t.Run("simple", func(t *testing.T) {
major, minor, patch, err := SemVer("1.2.3").Split()
require.NoError(t, err)
assert.Equal(t, 1, major)
assert.Equal(t, 2, minor)
assert.Equal(t, 3, patch)
})
t.Run("with suffix", func(t *testing.T) {
t.Run("single", func(t *testing.T) {
major, minor, patch, err := SemVer("1.2.3-pre").Split()
require.NoError(t, err)
assert.Equal(t, 1, major)
assert.Equal(t, 2, minor)
assert.Equal(t, 3, patch)
})
t.Run("multiple", func(t *testing.T) {
major, minor, patch, err := SemVer("1.2.3-pre-pre-pre-pre").Split()
require.NoError(t, err)
assert.Equal(t, 1, major)
assert.Equal(t, 2, minor)
assert.Equal(t, 3, patch)
})
})
t.Run("invalid", func(t *testing.T) {
_, _, _, err := SemVer("1.2.a").Split()
require.Error(t, err)
})
}
func TestSemVer_Number(t *testing.T) {
vn, err := SemVer("1.2.3").Number()
require.NoError(t, err)
assert.Equal(t, 3+(2<<10)+(1<<20), vn)
}
func TestSemVer_Compare(t *testing.T) {
t.Run("suffix is ignored", func(t *testing.T) {
cmp, err := SemVer("1.2.3-pre").Compare("1.2.3")
require.NoError(t, err)
assert.Zero(t, cmp)
})
t.Run("compare", func(t *testing.T) {
type tc struct {
a string
b string
exp int
}
for _, cas := range []tc{
{"1.2.3", "1.2.3", 0},
{"1.0.0", "2.0.0", -1},
{"1.0.0", "0.0.0", 1},
{"1.2.3", "1.3.3", -1},
{"1.2.3", "1.1.3", 1},
{"1.2.3", "1.2.4", -1},
{"1.2.3", "1.2.2", 1},
} {
t.Run(fmt.Sprintf("%q vs %q is %d", cas.a, cas.b, cas.exp), func(t *testing.T) {
cmp, err := SemVer(cas.a).Compare(SemVer(cas.b))
require.NoError(t, err)
switch cas.exp {
case 0:
assert.Zero(t, cmp)
case -1:
assert.Negative(t, cmp)
case 1:
assert.Positive(t, cmp)
}
})
}
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment