diff --git a/cmd/analyze.go b/cmd/analyze.go index f971c928..ba3afdd4 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -12,12 +12,12 @@ import ( "github.com/urfave/cli/v2" ) -func humanf(arg interface{}) string { +func humanf(arg float64) string { return color.Wrap(color.BrightWhite, humanize.Hf(arg)) } func writeAggrOutput(writer multiterm.MultilineTerm, aggr *aggregation.MatchNumerical, extra bool, quantiles []float64) int { - writer.WriteForLinef(0, "Samples: %v", color.Wrap(color.BrightWhite, humanize.Hi(aggr.Count()))) + writer.WriteForLinef(0, "Samples: %v", color.Wrap(color.BrightWhite, humanize.Hui(aggr.Count()))) writer.WriteForLinef(1, "Mean: %v", humanf(aggr.Mean())) writer.WriteForLinef(2, "StdDev: %v", humanf(aggr.StdDev())) writer.WriteForLinef(3, "Min: %v", humanf(aggr.Min())) diff --git a/cmd/expressions.go b/cmd/expressions.go index 39d817ef..8a3b7522 100644 --- a/cmd/expressions.go +++ b/cmd/expressions.go @@ -77,7 +77,7 @@ func expressionFunction(c *cli.Context) error { duration, iterations := exprofiler.Benchmark(compiled, &expCtx) perf := (duration / time.Duration(iterations)).String() fmt.Printf("Benchmark: %s ", color.Wrap(color.BrightWhite, perf)) - fmt.Print(color.Wrapf(color.BrightBlack, "(%s iterations in %s)", humanize.Hi(iterations), duration.String())) + fmt.Print(color.Wrapf(color.BrightBlack, "(%s iterations in %s)", humanize.Hi32(iterations), duration.String())) fmt.Print("\n") } diff --git a/cmd/helpers/summary.go b/cmd/helpers/summary.go index 258283db..7ce415ec 100644 --- a/cmd/helpers/summary.go +++ b/cmd/helpers/summary.go @@ -12,8 +12,8 @@ import ( func FWriteMatchSummary(w io.Writer, matched, total uint64) { fmt.Fprintf(w, "Matched: %s / %s", - color.Wrapi(color.BrightGreen, humanize.Hi(matched)), - color.Wrapi(color.BrightWhite, humanize.Hi(total))) + color.Wrapi(color.BrightGreen, humanize.Hui(matched)), + color.Wrapi(color.BrightWhite, humanize.Hui(total))) } func FWriteExtractorSummary(extractor *extractor.Extractor, errors uint64, additionalParts ...string) string { @@ -24,10 +24,10 @@ func FWriteExtractorSummary(extractor *extractor.Extractor, errors uint64, addit w.WriteString(p) } if extractor.IgnoredLines() > 0 { - fmt.Fprintf(&w, " (Ignored: %s)", color.Wrapi(color.Red, humanize.Hi(extractor.IgnoredLines()))) + fmt.Fprintf(&w, " (Ignored: %s)", color.Wrapi(color.Red, humanize.Hui(extractor.IgnoredLines()))) } if errors > 0 { - fmt.Fprintf(&w, " %s", color.Wrapf(color.Red, "(Errors: %v)", humanize.Hi(errors))) + fmt.Fprintf(&w, " %s", color.Wrapf(color.Red, "(Errors: %v)", humanize.Hui(errors))) } return w.String() } diff --git a/go.mod b/go.mod index a6411613..384c5096 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/tidwall/gjson v1.14.1 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 golang.org/x/term v0.0.0-20210503060354-a79de5458b56 - golang.org/x/text v0.3.7 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect honnef.co/go/tools v0.3.2 ) diff --git a/go.sum b/go.sum index d0826691..6e324da2 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,6 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjq golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f h1:OKYpQQVE3DKSc3r3zHVzq46vq5YH7x8xpR3/k9ixmUg= golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/expressions/stdlib/funcsStrings.go b/pkg/expressions/stdlib/funcsStrings.go index 6a93b923..c5071bc4 100644 --- a/pkg/expressions/stdlib/funcsStrings.go +++ b/pkg/expressions/stdlib/funcsStrings.go @@ -172,7 +172,7 @@ func kfHumanizeInt(args []KeyBuilderStage) KeyBuilderStage { if err != nil { return ErrorType } - return humanize.Hi(val) + return humanize.Hi32(val) }) } diff --git a/pkg/humanize/humanize.go b/pkg/humanize/humanize.go index 14cb8d86..b3f92cf6 100644 --- a/pkg/humanize/humanize.go +++ b/pkg/humanize/humanize.go @@ -1,46 +1,43 @@ package humanize import ( - "fmt" "strconv" - - "golang.org/x/text/language" - "golang.org/x/text/message" ) -var printer = message.NewPrinter(language.English) - // Enabled determines whether to use language message printer, or fmt var Enabled = true var Decimals = 4 -// H humanizes the output -func H(format string, args ...interface{}) string { +func Hi(arg int64) string { if !Enabled { - return fmt.Sprintf(format, args...) + return strconv.FormatInt(arg, 10) } - return printer.Sprintf(format, args...) + return humanizeInt(arg) } -func Hi(arg interface{}) string { +func Hui(arg uint64) string { if !Enabled { - return fmt.Sprintf("%d", arg) + return strconv.FormatUint(arg, 10) } - return printer.Sprintf("%d", arg) + return humanizeInt(arg) } -func Hf(arg interface{}) string { +func Hi32(arg int) string { if !Enabled { - return fmt.Sprintf("%f", arg) + return strconv.Itoa(arg) } - return printer.Sprintf("%.[2]*[1]f", arg, Decimals) + return humanizeInt(arg) +} + +func Hf(arg float64) string { + return Hfd(arg, Decimals) } -func Hfd(arg interface{}, decimals int) string { +func Hfd(arg float64, decimals int) string { if !Enabled { - return fmt.Sprintf("%f", arg) + return strconv.FormatFloat(arg, 'f', decimals, 64) } - return printer.Sprintf("%.[2]*[1]f", arg, decimals) + return humanizeFloat(arg, decimals) } var byteSizes = [...]string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"} diff --git a/pkg/humanize/humanize_test.go b/pkg/humanize/humanize_test.go index 38b6f7a9..ee384ae3 100644 --- a/pkg/humanize/humanize_test.go +++ b/pkg/humanize/humanize_test.go @@ -6,16 +6,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestH(t *testing.T) { - assert.Equal(t, "Hello 1,000", H("Hello %d", 1000)) -} - func TestHDisabled(t *testing.T) { Enabled = false - assert.Equal(t, "Hello 1000", H("Hello %d", 1000)) assert.Equal(t, "1000", Hi(1000)) - assert.Equal(t, "1000.000000", Hf(1000.0)) - assert.Equal(t, "1000.000000", Hfd(1000.0, 5)) + assert.Equal(t, "1000", Hui(1000)) + assert.Equal(t, "1000", Hi32(1000)) + assert.Equal(t, "1000.0000", Hf(1000.0)) + assert.Equal(t, "1000.00000", Hfd(1000.0, 5)) assert.Equal(t, "12341234", ByteSize(12341234)) Enabled = true } @@ -24,6 +21,14 @@ func TestHi(t *testing.T) { assert.Equal(t, "1,500", Hi(1500)) } +func TestHui(t *testing.T) { + assert.Equal(t, "1,500", Hui(1500)) +} + +func TestHi32(t *testing.T) { + assert.Equal(t, "1,500", Hi32(1500)) +} + func TestHf(t *testing.T) { assert.Equal(t, "1,234,567.8912", Hf(1234567.89121111)) } diff --git a/pkg/humanize/numeric.go b/pkg/humanize/numeric.go new file mode 100644 index 00000000..80c64aad --- /dev/null +++ b/pkg/humanize/numeric.go @@ -0,0 +1,122 @@ +package humanize + +import ( + "bytes" + "math" + "strconv" +) + +/* +Previously, rare used the `message` i18n go library to add commas to numbers, but +as it turns out that was a bit overkill (Benchmarking shows easily 10x slower, and added 600 KB to the +binary). In an effort to pull out and streamline simpler parts of the overall process, +the two below functions are implementations of the simplistic english-only localization +of numbers +*/ + +const ( + baseSeparator = ',' + decimalSeparator = '.' +) + +type IntType interface { + int64 | int32 | uint64 | uint32 | int | uint +} + +func humanizeInt[T IntType](v T) string { + var buf [32]byte // stack alloc + + if v >= 0 && v < 100 { // faster for small numbers + return strconv.FormatInt(int64(v), 10) + } + + negative := v < 0 + if negative { + v = -v + } + + ci := 0 + idx := len(buf) - 1 + for v > 0 { + if ci == 3 { + buf[idx] = baseSeparator + ci = 0 + idx-- + } + + buf[idx] = byte('0' + (v % 10)) + idx-- + ci++ + v /= 10 + } + + if negative { + buf[idx] = '-' + idx-- + } + + return string(buf[idx+1:]) +} + +func isDigit(s byte) bool { + return s >= '0' && s <= '9' +} + +func humanizeFloat(v float64, decimals int) string { + // Special cases + if math.IsNaN(v) { + return "NaN" + } + if math.IsInf(v, 0) { + return "Inf" + } + + // Float to string is complicated, but can leverage FormatFload and insert commas + var buf [64]byte // Operations on the stack + s := strconv.AppendFloat(buf[:0], v, 'f', decimals, 64) + + if v > -1000.0 && v < 1000.0 { + // performance escape hatch when no commas + return string(s) + } + + negative := s[0] == '-' + if !isDigit(s[0]) { // assume it's a sign/prefix + s = s[1:] + } + + decIdx := bytes.IndexByte(s, '.') + if decIdx < 0 { // no decimal + decIdx = len(s) + } + + // Return stack buf + var retbuf [64]byte + ret := retbuf[:0] + + if negative { + ret = append(ret, '-') + } + + // write base + c3 := 3 - (decIdx % 3) + + for i := 0; i < decIdx; i++ { + if c3 == 3 { + if i > 0 { + ret = append(ret, baseSeparator) + } + c3 = 0 + } + ret = append(ret, s[i]) + c3++ + } + + // write decimal + if decIdx < len(s) { + ret = append(ret, decimalSeparator) + ret = append(ret, s[decIdx+1:]...) + } + + return string(ret) +} diff --git a/pkg/humanize/numeric_test.go b/pkg/humanize/numeric_test.go new file mode 100644 index 00000000..b28a7b82 --- /dev/null +++ b/pkg/humanize/numeric_test.go @@ -0,0 +1,67 @@ +package humanize + +import ( + "math" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormatInt(t *testing.T) { + assert.Equal(t, "0", humanizeInt(0)) + assert.Equal(t, "1", humanizeInt(1)) + assert.Equal(t, "-1", humanizeInt(-1)) + assert.Equal(t, "10", humanizeInt(10)) + assert.Equal(t, "100", humanizeInt(100)) + assert.Equal(t, "1,000", humanizeInt(1000)) + assert.Equal(t, "10,000", humanizeInt(10000)) + + assert.Equal(t, "-100", humanizeInt(-100)) + assert.Equal(t, "-1,000", humanizeInt(-1000)) + assert.Equal(t, "-123,123", humanizeInt(-123123)) +} + +func TestFormatFloat(t *testing.T) { + assert.Equal(t, "0", humanizeFloat(0.0, 0)) + assert.Equal(t, "0.00", humanizeFloat(0.0, 2)) + assert.Equal(t, "1", humanizeFloat(1.0, 0)) + assert.Equal(t, "12", humanizeFloat(12.0, 0)) + assert.Equal(t, "123", humanizeFloat(123.0, 0)) + assert.Equal(t, "1,234", humanizeFloat(1234.0, 0)) + assert.Equal(t, "12,345.0", humanizeFloat(12345.0, 1)) + assert.Equal(t, "112,345.0", humanizeFloat(112345.0, 1)) + assert.Equal(t, "1", humanizeFloat(1.123, 0)) + assert.Equal(t, "-1", humanizeFloat(-1.123, 0)) + assert.Equal(t, "1,123,123", humanizeFloat(1123123.123, 0)) + assert.Equal(t, "-1,123,123", humanizeFloat(-1123123.123, 0)) + assert.Equal(t, "1,123,123.12", humanizeFloat(1123123.123, 2)) + assert.Equal(t, "1,123,123.123456", humanizeFloat(1123123.123456, 6)) + assert.Equal(t, "-1,123,123.123456", humanizeFloat(-1123123.123456, 6)) + assert.Equal(t, "-111,121,231,233,123.125000", humanizeFloat(-111121231233123.123456, 6)) + assert.Equal(t, "111,121,231,233,123.125000", humanizeFloat(111121231233123.123456, 6)) + assert.Equal(t, "28,446,744,073,709,551,616.0", humanizeFloat(28446744073709551615.0, 1)) + + assert.Equal(t, "NaN", humanizeFloat(math.NaN(), 2)) + assert.Equal(t, "Inf", humanizeFloat(math.Inf(1), 2)) + assert.Equal(t, "Inf", humanizeFloat(math.Inf(-1), 2)) +} + +func BenchmarkFormatInt(b *testing.B) { + for i := 0; i < b.N; i++ { + humanizeInt(10000) + } +} + +func BenchmarkItoa(b *testing.B) { + for i := 0; i < b.N; i++ { + strconv.Itoa(10000) + } +} + +// BenchmarkFormatFloat-4 2549425 473.6 ns/op 24 B/op 1 allocs/op +func BenchmarkFormatFloat(b *testing.B) { + for i := 0; i < b.N; i++ { + humanizeFloat(10000.123123123123, 10) + } +}