New custom SemVer implementation (#8010)

This was something I originally intended to use in another project,
but since this turned out to be a better implementation (subject of
course to review!) in terms of both performance and simplicity, I
figured we might as well use it here too. Some changes include:

- `MustParse` to parse known valid versions
- Allocates nothing in any operations other than 1 alloc in `String()`
- Ignores empty `PreRelease` and `Metadata` fields in serialization
- `encoding.TextAppender` implementation to serialize without allocating
- A whole bunch of benchmarks

Signed-off-by: Anders Eknert <anders@eknert.com>
This commit is contained in:
Anders Eknert
2025-11-04 09:09:50 +01:00
committed by GitHub
parent 4c31c81a0f
commit 30d3346fb4
9 changed files with 598 additions and 918 deletions
+1 -4
View File
@@ -22,10 +22,7 @@ func minVersionIndex() ast.VersionIndex {
}
for _, v := range versions {
var sv semver.Version
if err := sv.Set(v[1:]); err != nil {
panic(err)
}
sv := semver.MustParse(v[1:])
c, err := ast.LoadCapabilitiesVersion(v)
if err != nil {
+6 -10
View File
@@ -81,8 +81,6 @@ type GHResponse struct {
// New returns an instance of the Reporter
func New(opts Options) (Reporter, error) {
r := GHVersionCollector{}
url := cmp.Or(os.Getenv("OPA_TELEMETRY_SERVICE_URL"), ExternalServiceURL)
restConfig := fmt.Appendf(nil, `{
@@ -93,7 +91,7 @@ func New(opts Options) (Reporter, error) {
if err != nil {
return nil, err
}
r.client = client
r := GHVersionCollector{client: client}
// heap_usage_bytes is always present, so register it unconditionally
r.RegisterGatherer("heap_usage_bytes", readRuntimeMemStats)
@@ -135,19 +133,17 @@ func createDataResponse(ghResp GHResponse) (*DataResponse, error) {
return nil, errors.New("server response does not contain tag_name")
}
v := strings.TrimPrefix(version.Version, "v")
sv, err := semver.NewVersion(v)
sv, err := semver.Parse(version.Version)
if err != nil {
return nil, fmt.Errorf("failed to parse current version %q: %w", v, err)
return nil, fmt.Errorf("failed to parse current version %q: %w", version.Version, err)
}
latestV := strings.TrimPrefix(ghResp.TagName, "v")
latestSV, err := semver.NewVersion(latestV)
latestSV, err := semver.Parse(ghResp.TagName)
if err != nil {
return nil, fmt.Errorf("failed to parse latest version %q: %w", latestV, err)
return nil, fmt.Errorf("failed to parse latest version %q: %w", ghResp.TagName, err)
}
isLatest := sv.Compare(*latestSV) >= 0
isLatest := sv.Compare(latestSV) >= 0
// Note: alternatively, we could look through the assets in the GH API response to find a matching asset,
// and use its URL. However, this is not guaranteed to be more robust, and wouldn't use the 'openpolicyagent.org' domain.
+194 -197
View File
@@ -14,237 +14,234 @@
// Semantic Versions http://semver.org
// Package semver has been vendored from:
// This file was originally vendored from:
// https://github.com/coreos/go-semver/tree/e214231b295a8ea9479f11b70b35d5acf3556d9b/semver
// A number of the original functions of the package have been removed since
// they are not required for our built-ins.
// There isn't a single line left from the original source today, but being generous about
// attribution won't hurt.
package semver
import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/open-policy-agent/opa/v1/util"
)
// reMetaIdentifier matches pre-release and metadata identifiers against the spec requirements
var reMetaIdentifier = regexp.MustCompile(`^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$`)
// Version represents a parsed SemVer
type Version struct {
Major int64
Minor int64
Patch int64
PreRelease PreRelease
Metadata string
PreRelease string `json:"PreRelease,omitempty"`
Metadata string `json:"Metadata,omitempty"`
}
// PreRelease represents a pre-release suffix string
type PreRelease string
// Parse constructs new semver Version from version string.
func Parse(version string) (v Version, err error) {
version = strings.TrimPrefix(version, "v")
func splitOff(input *string, delim string) (val string) {
parts := strings.SplitN(*input, delim, 2)
if len(parts) == 2 {
*input = parts[0]
val = parts[1]
version, v.Metadata = cut(version, '+')
if v.Metadata != "" && !reMetaIdentifier.MatchString(v.Metadata) {
return v, fmt.Errorf("invalid metadata identifier: %s", v.Metadata)
}
return val
version, v.PreRelease = cut(version, '-')
if v.PreRelease != "" && !reMetaIdentifier.MatchString(v.PreRelease) {
return v, fmt.Errorf("invalid pre-release identifier: %s", v.PreRelease)
}
if strings.Count(version, ".") != 2 {
return v, fmt.Errorf("%s should contain major, minor, and patch versions", version)
}
major, after := cut(version, '.')
if v.Major, err = strconv.ParseInt(major, 10, 64); err != nil {
return v, err
}
minor, after := cut(after, '.')
if v.Minor, err = strconv.ParseInt(minor, 10, 64); err != nil {
return v, err
}
if v.Patch, err = strconv.ParseInt(after, 10, 64); err != nil {
return v, err
}
return v, nil
}
// NewVersion constructs new SemVers from strings
func NewVersion(version string) (*Version, error) {
v := Version{}
if err := v.Set(version); err != nil {
return nil, err
// MustParse is like Parse but panics if the version string is invalid instead of returning an error.
func MustParse(version string) Version {
v, err := Parse(version)
if err != nil {
panic(err)
}
return &v, nil
return v
}
// Set parses and updates v from the given version string. Implements flag.Value
func (v *Version) Set(version string) error {
metadata := splitOff(&version, "+")
preRelease := PreRelease(splitOff(&version, "-"))
dotParts := strings.SplitN(version, ".", 3)
if len(dotParts) != 3 {
return fmt.Errorf("%s is not in dotted-tri format", version)
}
if err := validateIdentifier(string(preRelease)); err != nil {
return fmt.Errorf("failed to validate pre-release: %v", err)
}
if err := validateIdentifier(metadata); err != nil {
return fmt.Errorf("failed to validate metadata: %v", err)
}
parsed := make([]int64, 3)
for i, v := range dotParts[:3] {
val, err := strconv.ParseInt(v, 10, 64)
parsed[i] = val
if err != nil {
return err
}
}
v.Metadata = metadata
v.PreRelease = preRelease
v.Major = parsed[0]
v.Minor = parsed[1]
v.Patch = parsed[2]
return nil
}
func (v Version) String() string {
var buffer bytes.Buffer
fmt.Fprintf(&buffer, "%d.%d.%d", v.Major, v.Minor, v.Patch)
if v.PreRelease != "" {
fmt.Fprintf(&buffer, "-%s", v.PreRelease)
}
if v.Metadata != "" {
fmt.Fprintf(&buffer, "+%s", v.Metadata)
}
return buffer.String()
}
// Compare tests if v is less than, equal to, or greater than versionB,
// returning -1, 0, or +1 respectively.
func (v Version) Compare(versionB Version) int {
if cmp := recursiveCompare(v.Slice(), versionB.Slice()); cmp != 0 {
return cmp
}
return preReleaseCompare(v, versionB)
}
// Slice converts the comparable parts of the semver into a slice of integers.
func (v Version) Slice() []int64 {
return []int64{v.Major, v.Minor, v.Patch}
}
// Slice splits the pre-release suffix string
func (p PreRelease) Slice() []string {
preRelease := string(p)
return strings.Split(preRelease, ".")
}
func preReleaseCompare(versionA Version, versionB Version) int {
a := versionA.PreRelease
b := versionB.PreRelease
/* Handle the case where if two versions are otherwise equal it is the
* one without a PreRelease that is greater */
if len(a) == 0 && (len(b) > 0) {
return 1
} else if len(b) == 0 && (len(a) > 0) {
return -1
}
// If there is a prerelease, check and compare each part.
return recursivePreReleaseCompare(a.Slice(), b.Slice())
}
func recursiveCompare(versionA []int64, versionB []int64) int {
if len(versionA) == 0 {
return 0
}
a := versionA[0]
b := versionB[0]
if a > b {
return 1
} else if a < b {
return -1
}
return recursiveCompare(versionA[1:], versionB[1:])
}
func recursivePreReleaseCompare(versionA []string, versionB []string) int {
// A larger set of pre-release fields has a higher precedence than a smaller set,
// if all of the preceding identifiers are equal.
if len(versionA) == 0 {
if len(versionB) > 0 {
return -1
}
return 0
} else if len(versionB) == 0 {
// We're longer than versionB so return 1.
return 1
}
a := versionA[0]
b := versionB[0]
aInt := false
bInt := false
aI, err := strconv.Atoi(versionA[0])
if err == nil {
aInt = true
}
bI, err := strconv.Atoi(versionB[0])
if err == nil {
bInt = true
}
// Numeric identifiers always have lower precedence than non-numeric identifiers.
if aInt && !bInt {
return -1
} else if !aInt && bInt {
return 1
}
// Handle Integer Comparison
if aInt && bInt {
if aI > bI {
return 1
} else if aI < bI {
return -1
}
}
// Handle String Comparison
if a > b {
return 1
} else if a < b {
return -1
}
return recursivePreReleaseCompare(versionA[1:], versionB[1:])
}
// validateIdentifier makes sure the provided identifier satisfies semver spec
func validateIdentifier(id string) error {
if id != "" && !reIdentifier.MatchString(id) {
return fmt.Errorf("%s is not a valid semver identifier", id)
}
return nil
}
// reIdentifier is a regular expression used to check that pre-release and metadata
// identifiers satisfy the spec requirements
var reIdentifier = regexp.MustCompile(`^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$`)
// Compare compares two semver strings.
func Compare(a, b string) int {
aV, err := NewVersion(strings.TrimPrefix(a, "v"))
aV, err := Parse(a)
if err != nil {
return -1
}
bV, err := NewVersion(strings.TrimPrefix(b, "v"))
bV, err := Parse(b)
if err != nil {
return 1
}
return aV.Compare(*bV)
return aV.Compare(bV)
}
// AppendText appends the textual representation of the version to b and returns the extended buffer.
// This method conforms to the encoding.TextAppender interface, and is useful for serializing the Version
// without allocating, provided the caller has pre-allocated sufficient space in b.
func (v Version) AppendText(b []byte) ([]byte, error) {
if b == nil {
b = make([]byte, 0, length(v))
}
b = append(strconv.AppendInt(b, v.Major, 10), '.')
b = append(strconv.AppendInt(b, v.Minor, 10), '.')
b = strconv.AppendInt(b, v.Patch, 10)
if v.PreRelease != "" {
b = append(append(b, '-'), v.PreRelease...)
}
if v.Metadata != "" {
b = append(append(b, '+'), v.Metadata...)
}
return b, nil
}
// String returns the string representation of the version.
func (v Version) String() string {
bs := make([]byte, 0, length(v))
bs, _ = v.AppendText(bs)
return string(bs)
}
// Compare tests if v is less than, equal to, or greater than other, returning -1, 0, or +1 respectively.
// Comparison is based on the SemVer specification (https://semver.org/#spec-item-11).
func (v Version) Compare(other Version) int {
if v.Major > other.Major {
return 1
} else if v.Major < other.Major {
return -1
}
if v.Minor > other.Minor {
return 1
} else if v.Minor < other.Minor {
return -1
}
if v.Patch > other.Patch {
return 1
} else if v.Patch < other.Patch {
return -1
}
if v.PreRelease == other.PreRelease {
return 0
}
// if two versions are otherwise equal it is the one without a pre-release that is greater
if v.PreRelease == "" && other.PreRelease != "" {
return 1
}
if other.PreRelease == "" && v.PreRelease != "" {
return -1
}
a, afterA := cut(v.PreRelease, '.')
b, afterB := cut(other.PreRelease, '.')
for {
if a == "" && b != "" {
return -1
}
if a != "" && b == "" {
return 1
}
aIsInt := isAllDecimals(a)
bIsInt := isAllDecimals(b)
// numeric identifiers have lower precedence than non-numeric
if aIsInt && !bIsInt {
return -1
} else if !aIsInt && bIsInt {
return 1
}
if aIsInt && bIsInt {
aInt, _ := strconv.Atoi(a)
bInt, _ := strconv.Atoi(b)
if aInt > bInt {
return 1
} else if aInt < bInt {
return -1
}
} else {
// string comparison
if a > b {
return 1
} else if a < b {
return -1
}
}
// a larger set of pre-release fields has a higher precedence than a
// smaller set, if all of the preceding identifiers are equal.
if afterA != "" && afterB == "" {
return 1
} else if afterA == "" && afterB != "" {
return -1
}
a, afterA = cut(afterA, '.')
b, afterB = cut(afterB, '.')
}
}
func isAllDecimals(s string) bool {
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return s != ""
}
// length allows calculating the length of the version for pre-allocation.
func length(v Version) int {
n := util.NumDigitsInt64(v.Major) + util.NumDigitsInt64(v.Minor) + util.NumDigitsInt64(v.Patch) + 2
if v.PreRelease != "" {
n += len(v.PreRelease) + 1
}
if v.Metadata != "" {
n += len(v.Metadata) + 1
}
return n
}
// cut is a *slightly* faster version of strings.Cut only accepting
// single byte separators, and skipping the boolean return value.
func cut(s string, sep byte) (before, after string) {
if i := strings.IndexByte(s, sep); i >= 0 {
return s[:i], s[i+1:]
}
return s, ""
}
+168 -57
View File
@@ -12,76 +12,98 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// vendored from: https://github.com/coreos/go-semver/tree/e214231b295a8ea9479f11b70b35d5acf3556d9b/semver
// originally vendored from: https://github.com/coreos/go-semver/tree/e214231b295a8ea9479f11b70b35d5acf3556d9b/semver
package semver
import (
"testing"
)
type fixture struct {
GreaterVersion string
LesserVersion string
}
type (
fixture struct {
greater string
lesser string
}
var fixtures = []fixture{
{"0.0.0", "0.0.0-foo"},
{"0.0.1", "0.0.0"},
{"1.0.0", "0.9.9"},
{"0.10.0", "0.9.0"},
{"0.99.0", "0.10.0"},
{"2.0.0", "1.2.3"},
{"0.0.0", "0.0.0-foo"},
{"0.0.1", "0.0.0"},
{"1.0.0", "0.9.9"},
{"0.10.0", "0.9.0"},
{"0.99.0", "0.10.0"},
{"2.0.0", "1.2.3"},
{"0.0.0", "0.0.0-foo"},
{"0.0.1", "0.0.0"},
{"1.0.0", "0.9.9"},
{"0.10.0", "0.9.0"},
{"0.99.0", "0.10.0"},
{"2.0.0", "1.2.3"},
{"1.2.3", "1.2.3-asdf"},
{"1.2.3", "1.2.3-4"},
{"1.2.3", "1.2.3-4-foo"},
{"1.2.3-5-foo", "1.2.3-5"},
{"1.2.3-5", "1.2.3-4"},
{"1.2.3-5-foo", "1.2.3-5-Foo"},
{"3.0.0", "2.7.2+asdf"},
{"3.0.0+foobar", "2.7.2"},
{"1.2.3-a.10", "1.2.3-a.5"},
{"1.2.3-a.b", "1.2.3-a.5"},
{"1.2.3-a.b", "1.2.3-a"},
{"1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"},
{"1.0.0", "1.0.0-rc.1"},
{"1.0.0-rc.2", "1.0.0-rc.1"},
{"1.0.0-rc.1", "1.0.0-beta.11"},
{"1.0.0-beta.11", "1.0.0-beta.2"},
{"1.0.0-beta.2", "1.0.0-beta"},
{"1.0.0-beta", "1.0.0-alpha.beta"},
{"1.0.0-alpha.beta", "1.0.0-alpha.1"},
{"1.0.0-alpha.1", "1.0.0-alpha"},
{"1.2.3-rc.1-1-1hash", "1.2.3-rc.2"},
compareFixture struct {
greater Version
lesser Version
}
)
var (
fixtures = []fixture{
{"0.0.0", "0.0.0-foo"},
{"0.0.1", "0.0.0"},
{"1.0.0", "0.9.9"},
{"0.10.0", "0.9.0"},
{"0.99.0", "0.10.0"},
{"2.0.0", "1.2.3"},
{"0.0.0", "0.0.0-foo"},
{"0.0.1", "0.0.0"},
{"1.0.0", "0.9.9"},
{"0.10.0", "0.9.0"},
{"0.99.0", "0.10.0"},
{"2.0.0", "1.2.3"},
{"0.0.0", "0.0.0-foo"},
{"0.0.1", "0.0.0"},
{"1.0.0", "0.9.9"},
{"0.10.0", "0.9.0"},
{"0.99.0", "0.10.0"},
{"2.0.0", "1.2.3"},
{"1.2.3", "1.2.3-asdf"},
{"1.2.3", "1.2.3-4"},
{"1.2.3", "1.2.3-4-foo"},
{"1.2.3-5-foo", "1.2.3-5"},
{"1.2.3-5", "1.2.3-4"},
{"1.2.3-5-foo", "1.2.3-5-Foo"},
{"3.0.0", "2.7.2+asdf"},
{"3.0.0+foobar", "2.7.2"},
{"1.2.3-a.10", "1.2.3-a.5"},
{"1.2.3-a.b", "1.2.3-a.5"},
{"1.2.3-a.b", "1.2.3-a"},
{"1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"},
{"1.0.0", "1.0.0-rc.1"},
{"1.0.0-rc.2", "1.0.0-rc.1"},
{"1.0.0-rc.1", "1.0.0-beta.11"},
{"1.0.0-beta.11", "1.0.0-beta.2"},
{"1.0.0-beta.2", "1.0.0-beta"},
{"1.0.0-beta", "1.0.0-alpha.beta"},
{"1.0.0-alpha.beta", "1.0.0-alpha.1"},
{"1.0.0-alpha.1", "1.0.0-alpha"},
{"1.2.3-rc.1-1-1hash", "1.2.3-rc.2"},
}
equalFixtures = []string{
"0.0.0",
"1.2.3",
"1.2.3-rc.1",
"1.2.3+build.123",
"1.2.3-rc.1+build.123",
"1.2.3-rc.1+build.123.444",
}
)
func TestCompareEqual(t *testing.T) {
for _, v := range equalFixtures {
a := MustParse(v)
o := a
if a.Compare(o) != 0 {
t.Errorf("Expected %s to be equal to %s", a, o)
}
}
}
func TestCompare(t *testing.T) {
for _, v := range fixtures {
gt, err := NewVersion(v.GreaterVersion)
if err != nil {
t.Error(err)
}
gt := MustParse(v.greater)
lt := MustParse(v.lesser)
lt, err := NewVersion(v.LesserVersion)
if err != nil {
t.Error(err)
}
if gt.Compare(*lt) <= 0 {
if gt.Compare(lt) <= 0 {
t.Errorf("%s should be greater than %s", gt, lt)
}
if lt.Compare(*gt) > 0 {
if lt.Compare(gt) > 0 {
t.Errorf("%s should not be greater than %s", lt, gt)
}
}
@@ -98,8 +120,97 @@ func TestBadInput(t *testing.T) {
"0.88.0+11_e4e5dcabb",
}
for _, b := range bad {
if _, err := NewVersion(b); err == nil {
if _, err := Parse(b); err == nil {
t.Error("Improperly accepted value: ", b)
}
}
}
func BenchmarkCompare(b *testing.B) {
versionFixtures := make([]compareFixture, 0, len(fixtures))
for _, v := range fixtures {
versionFixtures = append(versionFixtures, compareFixture{
greater: MustParse(v.greater),
lesser: MustParse(v.lesser),
})
}
for b.Loop() {
for _, v := range versionFixtures {
if v.greater.Compare(v.lesser) <= 0 {
b.Fatalf("%s should be greater than %s", v.greater, v.lesser)
}
if v.lesser.Compare(v.greater) > 0 {
b.Fatalf("%s should not be greater than %s", v.lesser, v.greater)
}
}
}
}
func BenchmarkCompareEqual(b *testing.B) {
versionFixtures := make([]Version, 0, len(equalFixtures))
for _, v := range equalFixtures {
versionFixtures = append(versionFixtures, MustParse(v))
}
for b.Loop() {
for _, v := range versionFixtures {
o := v
if v.Compare(o) != 0 {
b.Fatalf("Expected %s to be equal to %s", v, o)
}
}
}
}
func BenchmarkString(b *testing.B) {
v := MustParse("1.2.3-alpha.1+build.123")
for b.Loop() {
if s := v.String(); s != "1.2.3-alpha.1+build.123" {
b.Fatalf("unexpected version string: %s", s)
}
}
}
func BenchmarkAppendText(b *testing.B) {
v := MustParse("1.2.3-alpha.1+build.123")
for b.Loop() {
_, err := v.AppendText(nil)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkAppendTextPreAllocated(b *testing.B) {
v, err := Parse("1.2.3-alpha.1+build.123")
if err != nil {
b.Fatal(err)
}
buf := make([]byte, 0, 32)
for b.Loop() {
if buf, err = v.AppendText(buf); err != nil {
b.Fatal(err)
}
if string(buf) != "1.2.3-alpha.1+build.123" {
b.Fatal("unexpected version string")
}
buf = buf[:0]
}
}
func BenchmarkParseSimple(b *testing.B) {
for b.Loop() {
MustParse("1.2.3")
}
}
func BenchmarkParsePrereleaseAndMetadata(b *testing.B) {
for b.Loop() {
MustParse("1.2.3-alpha.1+build.123")
}
}
+1 -6
View File
@@ -228,13 +228,8 @@ func LoadCapabilitiesVersions() ([]string, error) {
// MinimumCompatibleVersion returns the minimum compatible OPA version based on
// the built-ins, features, and keywords in c.
func (c *Capabilities) MinimumCompatibleVersion() (string, bool) {
var maxVersion semver.Version
// this is the oldest OPA release that includes capabilities
if err := maxVersion.Set("0.17.0"); err != nil {
panic("unreachable")
}
maxVersion := semver.MustParse("0.17.0")
minVersionIndex := minVersionIndexOnce()
for _, bi := range c.Builtins {
+210 -624
View File
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -1268,11 +1268,12 @@ func compile(c *ast.Capabilities, b *bundle.Bundle, dbg debug.Debug, enablePrint
return nil, compiler.Errors
}
minVersion, ok := compiler.Required.MinimumCompatibleVersion()
if !ok {
dbg.Printf("could not determine minimum compatible version!")
} else {
dbg.Printf("minimum compatible version: %v", minVersion)
if dbg.Writer() != io.Discard {
if minVersion, ok := compiler.Required.MinimumCompatibleVersion(); !ok {
dbg.Printf("could not determine minimum compatible version!")
} else {
dbg.Printf("minimum compatible version: %v", minVersion)
}
}
return compiler, nil
+6 -15
View File
@@ -23,34 +23,25 @@ func builtinSemVerCompare(_ BuiltinContext, operands []*ast.Term, iter func(*ast
return err
}
versionA, err := semver.NewVersion(string(versionStringA))
versionA, err := semver.Parse(string(versionStringA))
if err != nil {
return fmt.Errorf("operand 1: string %s is not a valid SemVer", versionStringA)
}
versionB, err := semver.NewVersion(string(versionStringB))
versionB, err := semver.Parse(string(versionStringB))
if err != nil {
return fmt.Errorf("operand 2: string %s is not a valid SemVer", versionStringB)
}
result := versionA.Compare(*versionB)
return iter(ast.InternedTerm(result))
return iter(ast.InternedTerm(versionA.Compare(versionB)))
}
func builtinSemVerIsValid(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
versionString, err := builtins.StringOperand(operands[0].Value, 1)
if err != nil {
return iter(ast.InternedTerm(false))
if err == nil {
_, err = semver.Parse(string(versionString))
}
result := true
_, err = semver.NewVersion(string(versionString))
if err != nil {
result = false
}
return iter(ast.InternedTerm(result))
return iter(ast.InternedTerm(err == nil))
}
func init() {
+6
View File
@@ -44,6 +44,12 @@ func StringToByteSlice[T ~string](s T) []byte {
// NumDigitsInt returns the number of digits in n.
// This is useful for pre-allocating buffers for string conversion.
func NumDigitsInt(n int) int {
return NumDigitsInt64(int64(n))
}
// NumDigitsInt64 returns the number of digits in n.
// This is useful for pre-allocating buffers for string conversion.
func NumDigitsInt64(n int64) int {
if n == 0 {
return 1
}