feat(decimals): add decimal utils

This commit is contained in:
Gaze
2024-06-10 05:07:26 +07:00
parent e91c7db601
commit accf37a218
6 changed files with 317 additions and 0 deletions

1
go.mod
View File

@@ -45,6 +45,7 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect

2
go.sum
View File

@@ -112,6 +112,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=

93
pkg/decimals/decimals.go Normal file
View File

@@ -0,0 +1,93 @@
package decimals
import (
"math/big"
"reflect"
"github.com/Cleverse/go-utilities/utils"
"github.com/gaze-network/indexer-network/pkg/logger"
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
"github.com/gaze-network/uint128"
"github.com/holiman/uint256"
"github.com/shopspring/decimal"
)
const (
DefaultDivPrecision = 36
)
func init() {
decimal.DivisionPrecision = DefaultDivPrecision
}
// MustFromString convert string to decimal.Decimal. Panic if error
// string must be a valid number, not NaN, Inf or empty string.
func MustFromString(s string) decimal.Decimal {
return utils.Must(decimal.NewFromString(s))
}
// ToDecimal convert any type to decimal.Decimal (safety floating point)
func ToDecimal(ivalue any, decimals uint16) decimal.Decimal {
value := new(big.Int)
switch v := ivalue.(type) {
case string:
value.SetString(v, 10)
case *big.Int:
value = v
case int64:
value = big.NewInt(v)
case int, int8, int16, int32:
rValue := reflect.ValueOf(v)
value.SetInt64(rValue.Int())
case uint64:
value = big.NewInt(0).SetUint64(v)
case uint, uint8, uint16, uint32:
rValue := reflect.ValueOf(v)
value.SetUint64(rValue.Uint())
case []byte:
value.SetBytes(v)
case uint128.Uint128:
value = v.Big()
case uint256.Int:
value = v.ToBig()
case *uint256.Int:
value = v.ToBig()
}
return decimal.NewFromBigInt(value, -int32(decimals))
}
// ToBigInt convert any type to *big.Int
func ToBigInt(iamount any, decimals uint16) *big.Int {
amount := decimal.NewFromFloat(0)
switch v := iamount.(type) {
case string:
amount, _ = decimal.NewFromString(v)
case float64:
amount = decimal.NewFromFloat(v)
case float32:
amount = decimal.NewFromFloat32(v)
case int64:
amount = decimal.NewFromInt(v)
case int, int8, int16, int32:
rValue := reflect.ValueOf(v)
amount = decimal.NewFromInt(rValue.Int())
case decimal.Decimal:
amount = v
case *decimal.Decimal:
amount = *v
case big.Float:
amount, _ = decimal.NewFromString(v.String())
case *big.Float:
amount, _ = decimal.NewFromString(v.String())
}
return amount.Mul(PowerOfTen(decimals)).BigInt()
}
// ToUint256 convert any type to *uint256.Int
func ToUint256(iamount any, decimals uint16) *uint256.Int {
result := new(uint256.Int)
if overflow := result.SetFromBig(ToBigInt(iamount, decimals)); overflow {
logger.Panic("ToUint256 overflow", slogx.Any("amount", iamount), slogx.Uint16("decimals", decimals))
}
return result
}

View File

@@ -0,0 +1,80 @@
package decimals
import (
"fmt"
"math"
"math/big"
"testing"
"github.com/gaze-network/uint128"
"github.com/holiman/uint256"
"github.com/stretchr/testify/assert"
)
func TestToDecimal(t *testing.T) {
t.Run("check_supported_types", func(t *testing.T) {
testcases := []struct {
decimals uint16
value uint64
expected string
}{
{0, 1, "1"},
{1, 1, "0.1"},
{2, 1, "0.01"},
{3, 1, "0.001"},
{18, 1, "0.000000000000000001"},
{36, 1, "0.000000000000000000000000000000000001"},
}
typesConv := []func(uint64) any{
func(i uint64) any { return int(i) },
func(i uint64) any { return int8(i) },
func(i uint64) any { return int16(i) },
func(i uint64) any { return int32(i) },
func(i uint64) any { return int64(i) },
func(i uint64) any { return uint(i) },
func(i uint64) any { return uint8(i) },
func(i uint64) any { return uint16(i) },
func(i uint64) any { return uint32(i) },
func(i uint64) any { return uint64(i) },
func(i uint64) any { return fmt.Sprint(i) },
func(i uint64) any { return new(big.Int).SetUint64(i) },
func(i uint64) any { return new(uint128.Uint128).Add64(i) },
func(i uint64) any { return uint256.NewInt(i) },
}
for _, tc := range testcases {
t.Run(fmt.Sprintf("%d_%d", tc.decimals, tc.value), func(t *testing.T) {
for _, conv := range typesConv {
input := conv(tc.value)
t.Run(fmt.Sprintf("%T", input), func(t *testing.T) {
actual := ToDecimal(input, tc.decimals)
assert.Equal(t, tc.expected, actual.String())
})
}
})
}
})
testcases := []struct {
decimals uint16
value interface{}
expected string
}{
{0, uint64(math.MaxUint64), "18446744073709551615"},
{18, uint64(math.MaxUint64), "18.446744073709551615"},
{36, uint64(math.MaxUint64), "0.000000000000000018446744073709551615"},
/* max uint128 */
{0, uint128.Max, "340282366920938463463374607431768211455"},
{18, uint128.Max, "340282366920938463463.374607431768211455"},
{36, uint128.Max, "340.282366920938463463374607431768211455"},
/* max uint256 */
{0, new(uint256.Int).SetAllOne(), "115792089237316195423570985008687907853269984665640564039457584007913129639935"},
{18, new(uint256.Int).SetAllOne(), "115792089237316195423570985008687907853269984665640564039457.584007913129639935"},
{36, new(uint256.Int).SetAllOne(), "115792089237316195423570985008687907853269.984665640564039457584007913129639935"},
}
for _, tc := range testcases {
t.Run(fmt.Sprintf("%d_%s", tc.decimals, tc.value), func(t *testing.T) {
actual := ToDecimal(tc.value, tc.decimals)
assert.Equal(t, tc.expected, actual.String())
})
}
}

View File

@@ -0,0 +1,97 @@
package decimals
import (
"github.com/shopspring/decimal"
"golang.org/x/exp/constraints"
)
// max precision is 36
const (
minPowerOfTen = -DefaultDivPrecision
maxPowerOfTen = DefaultDivPrecision
)
var powerOfTen = map[int64]decimal.Decimal{
minPowerOfTen: MustFromString("0.000000000000000000000000000000000001"),
-35: MustFromString("0.00000000000000000000000000000000001"),
-34: MustFromString("0.0000000000000000000000000000000001"),
-33: MustFromString("0.000000000000000000000000000000001"),
-32: MustFromString("0.00000000000000000000000000000001"),
-31: MustFromString("0.0000000000000000000000000000001"),
-30: MustFromString("0.000000000000000000000000000001"),
-29: MustFromString("0.00000000000000000000000000001"),
-28: MustFromString("0.0000000000000000000000000001"),
-27: MustFromString("0.000000000000000000000000001"),
-26: MustFromString("0.00000000000000000000000001"),
-25: MustFromString("0.0000000000000000000000001"),
-24: MustFromString("0.000000000000000000000001"),
-23: MustFromString("0.00000000000000000000001"),
-22: MustFromString("0.0000000000000000000001"),
-21: MustFromString("0.000000000000000000001"),
-20: MustFromString("0.00000000000000000001"),
-19: MustFromString("0.0000000000000000001"),
-18: MustFromString("0.000000000000000001"),
-17: MustFromString("0.00000000000000001"),
-16: MustFromString("0.0000000000000001"),
-15: MustFromString("0.000000000000001"),
-14: MustFromString("0.00000000000001"),
-13: MustFromString("0.0000000000001"),
-12: MustFromString("0.000000000001"),
-11: MustFromString("0.00000000001"),
-10: MustFromString("0.0000000001"),
-9: MustFromString("0.000000001"),
-8: MustFromString("0.00000001"),
-7: MustFromString("0.0000001"),
-6: MustFromString("0.000001"),
-5: MustFromString("0.00001"),
-4: MustFromString("0.0001"),
-3: MustFromString("0.001"),
-2: MustFromString("0.01"),
-1: MustFromString("0.1"),
0: MustFromString("1"),
1: MustFromString("10"),
2: MustFromString("100"),
3: MustFromString("1000"),
4: MustFromString("10000"),
5: MustFromString("100000"),
6: MustFromString("1000000"),
7: MustFromString("10000000"),
8: MustFromString("100000000"),
9: MustFromString("1000000000"),
10: MustFromString("10000000000"),
11: MustFromString("100000000000"),
12: MustFromString("1000000000000"),
13: MustFromString("10000000000000"),
14: MustFromString("100000000000000"),
15: MustFromString("1000000000000000"),
16: MustFromString("10000000000000000"),
17: MustFromString("100000000000000000"),
18: MustFromString("1000000000000000000"),
19: MustFromString("10000000000000000000"),
20: MustFromString("100000000000000000000"),
21: MustFromString("1000000000000000000000"),
22: MustFromString("10000000000000000000000"),
23: MustFromString("100000000000000000000000"),
24: MustFromString("1000000000000000000000000"),
25: MustFromString("10000000000000000000000000"),
26: MustFromString("100000000000000000000000000"),
27: MustFromString("1000000000000000000000000000"),
28: MustFromString("10000000000000000000000000000"),
29: MustFromString("100000000000000000000000000000"),
30: MustFromString("1000000000000000000000000000000"),
31: MustFromString("10000000000000000000000000000000"),
32: MustFromString("100000000000000000000000000000000"),
33: MustFromString("1000000000000000000000000000000000"),
34: MustFromString("10000000000000000000000000000000000"),
35: MustFromString("100000000000000000000000000000000000"),
maxPowerOfTen: MustFromString("1000000000000000000000000000000000000"),
}
// PowerOfTen optimized arithmetic performance for 10^n.
func PowerOfTen[T constraints.Integer](n T) decimal.Decimal {
nInt64 := int64(n)
if val, ok := powerOfTen[nInt64]; ok {
return val
}
return powerOfTen[1].Pow(decimal.NewFromInt(nInt64))
}

View File

@@ -0,0 +1,44 @@
package decimals
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPowerOfTen(t *testing.T) {
for n := int64(-36); n <= 36; n++ {
t.Run(fmt.Sprint(n), func(t *testing.T) {
expected := powerOfTenString(n)
actual := PowerOfTen(n)
assert.Equal(t, expected, actual.String())
})
}
t.Run("constants", func(t *testing.T) {
for n, p := range powerOfTen {
t.Run(p.String(), func(t *testing.T) {
require.False(t, p.IsZero(), "power of ten must not be zero")
actual := PowerOfTen(n)
assert.Equal(t, p, actual)
})
}
})
}
// powerOfTenString add zero padding to power of ten string
func powerOfTenString(n int64) string {
s := "1"
if n < 0 {
for i := int64(0); i < -n-1; i++ {
s = "0" + s
}
s = "0." + s
} else {
for i := int64(0); i < n; i++ {
s = s + "0"
}
}
return s
}