Skip to content

Commit

Permalink
Local i18n (#85)
Browse files Browse the repository at this point in the history
Move the thousands-separator logic into rare, saving 600 KB, and increasing performance of the functions significantly
  • Loading branch information
zix99 committed Oct 17, 2022
1 parent 24c635c commit f7eaf18
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 37 deletions.
4 changes: 2 additions & 2 deletions cmd/analyze.go
Expand Up @@ -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()))
Expand Down
2 changes: 1 addition & 1 deletion cmd/expressions.go
Expand Up @@ -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")
}

Expand Down
8 changes: 4 additions & 4 deletions cmd/helpers/summary.go
Expand Up @@ -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 {
Expand All @@ -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()
}
Expand Down
1 change: 0 additions & 1 deletion go.mod
Expand Up @@ -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
)
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Expand Up @@ -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=
Expand Down
2 changes: 1 addition & 1 deletion pkg/expressions/stdlib/funcsStrings.go
Expand Up @@ -172,7 +172,7 @@ func kfHumanizeInt(args []KeyBuilderStage) KeyBuilderStage {
if err != nil {
return ErrorType
}
return humanize.Hi(val)
return humanize.Hi32(val)
})
}

Expand Down
35 changes: 16 additions & 19 deletions 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"}
Expand Down
19 changes: 12 additions & 7 deletions pkg/humanize/humanize_test.go
Expand Up @@ -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
}
Expand All @@ -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))
}
Expand Down
122 changes: 122 additions & 0 deletions 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)
}
67 changes: 67 additions & 0 deletions 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)
}
}

0 comments on commit f7eaf18

Please sign in to comment.