diff --git a/math/CHANGELOG.md b/math/CHANGELOG.md index 3b7c8e741f..502e2e1b36 100644 --- a/math/CHANGELOG.md +++ b/math/CHANGELOG.md @@ -31,5 +31,6 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +* [#14010](https://github.com/cosmos/cosmos-sdk/pull/14010) Optimize FormatInt to not do plain string concentation when formatting thousands and instead build it more efficiently. * [#13381](https://github.com/cosmos/cosmos-sdk/pull/13381) Add uint `IsNil` method. * [#12634](https://github.com/cosmos/cosmos-sdk/pull/12634) Move `sdk.Dec` to math package, call it `LegacyDec`. diff --git a/math/dec.go b/math/dec.go index 1ed82b9ef0..809a8e8616 100644 --- a/math/dec.go +++ b/math/dec.go @@ -178,7 +178,7 @@ func LegacyNewDecFromStr(str string) (LegacyDec, error) { // add some extra zero's to correct to the Precision factor zerosToAdd := LegacyPrecision - lenDecs - zeros := fmt.Sprintf(`%0`+strconv.Itoa(zerosToAdd)+`s`, "") + zeros := strings.Repeat("0", zerosToAdd) combinedStr += zeros combined, ok := new(big.Int).SetString(combinedStr, 10) // base 10 diff --git a/math/int.go b/math/int.go index 6811893f7a..184a524648 100644 --- a/math/int.go +++ b/math/int.go @@ -6,6 +6,7 @@ import ( "fmt" "math/big" "strings" + "sync" "testing" ) @@ -448,6 +449,10 @@ func hasOnlyDigits(s string) bool { const thousandSeparator string = "'" +var stringsBuilderPool = &sync.Pool{ + New: func() any { return new(strings.Builder) }, +} + // FormatInt formats an integer (encoded as in protobuf) into a value-rendered // string following ADR-050. This function operates with string manipulation // (instead of manipulating the int or sdk.Int object). @@ -466,11 +471,34 @@ func FormatInt(v string) (string, error) { return "", fmt.Errorf("expecting only digits 0-9, but got non-digits in %q", v) } - startOffset := 3 - for outputIndex := len(v); outputIndex > startOffset; { - outputIndex -= 3 - v = v[:outputIndex] + thousandSeparator + v[outputIndex:] + // 1. Less than 4 digits don't need any formatting. + if len(v) <= 3 { + return sign + v, nil } - return sign + v, nil + sb := stringsBuilderPool.Get().(*strings.Builder) + defer stringsBuilderPool.Put(sb) + sb.Reset() + sb.Grow(len(v) + len(v)/3) // Exactly v + numberOfThousandSeparatorsIn(v) + + // 2. If the length of v is not a multiple of 3 e.g. 1234 or 12345, to achieve 1'234 or 12'345, + // we can simply slide to the first mod3 values of v that aren't the multiples of 3 then insert in + // the thousands separator so in this case: write(12'); then the remaining v will be entirely multiple + // of 3 hence v = 34* + if mod3 := len(v) % 3; mod3 != 0 { + sb.WriteString(v[:mod3]) + v = v[mod3:] + sb.WriteString(thousandSeparator) + } + + // 3. By this point v is entirely multiples of 3 hence we just insert the separator at every 3 digit. + for i := 0; i < len(v); i += 3 { + end := i + 3 + sb.WriteString(v[i:end]) + if end < len(v) { + sb.WriteString(thousandSeparator) + } + } + + return sign + sb.String(), nil } diff --git a/math/int_test.go b/math/int_test.go index 5af450cb6c..4f5b31c0f1 100644 --- a/math/int_test.go +++ b/math/int_test.go @@ -472,3 +472,39 @@ func TestFormatIntNonDigits(t *testing.T) { }) } } + +func TestFormatIntCorrectness(t *testing.T) { + tests := []struct { + in string + want string + }{ + {"0", "0"}, + {"-2", "-2"}, + {"10", "10"}, + {"123", "123"}, + {"1234", "1'234"}, + {"12345", "12'345"}, + {"123456", "123'456"}, + {"-123456", "-123'456"}, + {"1234567", "1'234'567"}, + {"12345678", "12'345'678"}, + {"123456789", "123'456'789"}, + {"12345678910", "12'345'678'910"}, + {"9999999999999999", "9'999'999'999'999'999"}, + {"-9999999999999999", "-9'999'999'999'999'999"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.in, func(t *testing.T) { + got, err := math.FormatInt(tt.in) + if err != nil { + t.Fatal(err) + } + + if got != tt.want { + t.Fatalf("Mismatch:\n\tGot: %q\n\tWant: %q", got, tt.want) + } + }) + } +}