feat(math): Implement custom hybrid un-/marshal model (#22529)
This commit is contained in:
parent
00c7756610
commit
abc5fd0226
@ -36,6 +36,9 @@ Ref: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.j
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
* [#11783](https://github.com/cosmos/cosmos-sdk/issues/11783) feat(math): Upstream GDA based decimal type
|
||||
|
||||
|
||||
## [math/v1.4.0](https://github.com/cosmos/cosmos-sdk/releases/tag/math/v1.4.0) - 2024-01-20
|
||||
|
||||
### Features
|
||||
|
||||
82
math/dec.go
82
math/dec.go
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"math/big"
|
||||
"strconv"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
|
||||
@ -299,7 +300,7 @@ func (x Dec) Int64() (int64, error) {
|
||||
// fit precisely into an *big.Int.
|
||||
func (x Dec) BigInt() (*big.Int, error) {
|
||||
y, _ := x.Reduce()
|
||||
z, ok := new(big.Int).SetString(y.String(), 10)
|
||||
z, ok := new(big.Int).SetString(y.Text('f'), 10)
|
||||
if !ok {
|
||||
return nil, ErrNonIntegral
|
||||
}
|
||||
@ -334,7 +335,7 @@ func (x Dec) SdkIntTrim() (Int, error) {
|
||||
|
||||
// String formatted in decimal notation: '-ddddd.dddd', no exponent
|
||||
func (x Dec) String() string {
|
||||
return x.dec.Text('f')
|
||||
return string(fmtE(x.dec, 'E'))
|
||||
}
|
||||
|
||||
// Text converts the floating-point number x to a string according
|
||||
@ -407,14 +408,81 @@ func (x Dec) Reduce() (Dec, int) {
|
||||
return y, n
|
||||
}
|
||||
|
||||
// Marshal serializes the decimal value into a byte slice in text format.
|
||||
// This method represents the decimal in a portable and compact hybrid notation.
|
||||
// Based on the exponent value, the number is formatted into decimal: -ddddd.ddddd, no exponent
|
||||
// or scientific notation: -d.ddddE±dd
|
||||
//
|
||||
// For example, the following transformations are made:
|
||||
// - 0 -> 0
|
||||
// - 123 -> 123
|
||||
// - 10000 -> 10000
|
||||
// - -0.001 -> -0.001
|
||||
// - -0.000000001 -> -1E-9
|
||||
//
|
||||
// Returns:
|
||||
// - A byte slice of the decimal in text format.
|
||||
// - An error if the decimal cannot be reduced or marshaled properly.
|
||||
func (x Dec) Marshal() ([]byte, error) {
|
||||
// implemented in a new PR. See: https://github.com/cosmos/cosmos-sdk/issues/22525
|
||||
panic("not implemented")
|
||||
var d apd.Decimal
|
||||
if _, _, err := dec128Context.Reduce(&d, &x.dec); err != nil {
|
||||
return nil, ErrInvalidDec.Wrap(err.Error())
|
||||
}
|
||||
return fmtE(d, 'E'), nil
|
||||
}
|
||||
|
||||
// fmtE formats a decimal number into a byte slice in scientific notation or fixed-point notation depending on the exponent.
|
||||
// If the adjusted exponent is between -6 and 6 inclusive, it uses fixed-point notation, otherwise it uses scientific notation.
|
||||
func fmtE(d apd.Decimal, fmt byte) []byte {
|
||||
var scratch, dest [16]byte
|
||||
buf := dest[:0]
|
||||
digits := d.Coeff.Append(scratch[:0], 10)
|
||||
totalDigits := int64(len(digits))
|
||||
adj := int64(d.Exponent) + totalDigits - 1
|
||||
if adj > -6 && adj < 6 {
|
||||
return []byte(d.Text('f'))
|
||||
}
|
||||
switch {
|
||||
case totalDigits > 5:
|
||||
beforeComma := digits[0 : totalDigits-6]
|
||||
adj -= int64(len(beforeComma) - 1)
|
||||
buf = append(buf, beforeComma...)
|
||||
buf = append(buf, '.')
|
||||
buf = append(buf, digits[totalDigits-6:]...)
|
||||
case totalDigits > 1:
|
||||
buf = append(buf, digits[0])
|
||||
buf = append(buf, '.')
|
||||
buf = append(buf, digits[1:]...)
|
||||
default:
|
||||
buf = append(buf, digits[0:]...)
|
||||
}
|
||||
|
||||
buf = append(buf, fmt)
|
||||
var ch byte
|
||||
if adj < 0 {
|
||||
ch = '-'
|
||||
adj = -adj
|
||||
} else {
|
||||
ch = '+'
|
||||
}
|
||||
buf = append(buf, ch)
|
||||
return strconv.AppendInt(buf, adj, 10)
|
||||
}
|
||||
|
||||
// Unmarshal parses a byte slice containing a text-formatted decimal and stores the result in the receiver.
|
||||
// It returns an error if the byte slice does not represent a valid decimal.
|
||||
func (x *Dec) Unmarshal(data []byte) error {
|
||||
// implemented in a new PR. See: https://github.com/cosmos/cosmos-sdk/issues/22525
|
||||
panic("not implemented")
|
||||
result, err := NewDecFromString(string(data))
|
||||
if err != nil {
|
||||
return ErrInvalidDec.Wrap(err.Error())
|
||||
}
|
||||
|
||||
if result.dec.Form != apd.Finite {
|
||||
return ErrInvalidDec.Wrap("unknown decimal form")
|
||||
}
|
||||
|
||||
x.dec = result.dec
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalTo encodes the receiver into the provided byte slice and returns the number of bytes written and any error encountered.
|
||||
@ -435,7 +503,7 @@ func (x Dec) Size() int {
|
||||
|
||||
// MarshalJSON serializes the Dec struct into a JSON-encoded byte slice using scientific notation.
|
||||
func (x Dec) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(x.dec.Text('E'))
|
||||
return json.Marshal(fmtE(x.dec, 'E'))
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for the Dec type, converting JSON strings to Dec objects.
|
||||
|
||||
@ -308,7 +308,7 @@ func ExampleDec_MulExact() {
|
||||
// 2.50
|
||||
// exponent out of range: invalid decimal
|
||||
// unexpected rounding
|
||||
// 0.00000000000000000000000000000000000
|
||||
// 0E-35
|
||||
// 0
|
||||
}
|
||||
|
||||
|
||||
153
math/dec_test.go
153
math/dec_test.go
@ -3,7 +3,6 @@ package math
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -152,11 +151,11 @@ func TestNewDecFromInt64(t *testing.T) {
|
||||
},
|
||||
"max value": {
|
||||
src: math.MaxInt64,
|
||||
exp: strconv.Itoa(math.MaxInt64),
|
||||
exp: "9223372036854.775807E+6",
|
||||
},
|
||||
"min value": {
|
||||
src: math.MinInt64,
|
||||
exp: strconv.Itoa(math.MinInt64),
|
||||
exp: "9223372036854.775808E+6",
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
@ -1247,15 +1246,17 @@ func TestToBigInt(t *testing.T) {
|
||||
{"12345.6", "", ErrNonIntegral},
|
||||
}
|
||||
for idx, tc := range tcs {
|
||||
a, err := NewDecFromString(tc.intStr)
|
||||
require.NoError(t, err)
|
||||
b, err := a.BigInt()
|
||||
if tc.isError == nil {
|
||||
require.NoError(t, err, "test_%d", idx)
|
||||
require.Equal(t, tc.out, b.String(), "test_%d", idx)
|
||||
} else {
|
||||
require.ErrorIs(t, err, tc.isError, "test_%d", idx)
|
||||
}
|
||||
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
|
||||
a, err := NewDecFromString(tc.intStr)
|
||||
require.NoError(t, err)
|
||||
b, err := a.BigInt()
|
||||
if tc.isError == nil {
|
||||
require.NoError(t, err, "test_%d", idx)
|
||||
require.Equal(t, tc.out, b.String(), "test_%d", idx)
|
||||
} else {
|
||||
require.ErrorIs(t, err, tc.isError, "test_%d", idx)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1309,55 +1310,38 @@ func must[T any](r T, err error) T {
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshal(t *testing.T) {
|
||||
t.Skip("not supported, yet")
|
||||
specs := map[string]struct {
|
||||
x Dec
|
||||
exp string
|
||||
expErr error
|
||||
}{
|
||||
"No trailing zeros": {
|
||||
x: NewDecFromInt64(123456),
|
||||
exp: "1.23456E+5",
|
||||
},
|
||||
"Trailing zeros": {
|
||||
x: NewDecFromInt64(123456000),
|
||||
exp: "1.23456E+8",
|
||||
},
|
||||
"Zero value": {
|
||||
x: NewDecFromInt64(0),
|
||||
exp: "0E+0",
|
||||
exp: "0",
|
||||
},
|
||||
"-0": {
|
||||
x: NewDecFromInt64(-0),
|
||||
exp: "0E+0",
|
||||
exp: "0",
|
||||
},
|
||||
"Decimal value": {
|
||||
x: must(NewDecFromString("1.30000")),
|
||||
exp: "1.3E+0",
|
||||
"1 decimal place": {
|
||||
x: must(NewDecFromString("0.1")),
|
||||
exp: "0.1",
|
||||
},
|
||||
"Positive value": {
|
||||
x: NewDecFromInt64(10),
|
||||
exp: "1E+1",
|
||||
"2 decimal places": {
|
||||
x: must(NewDecFromString("0.01")),
|
||||
exp: "0.01",
|
||||
},
|
||||
"negative 10": {
|
||||
x: NewDecFromInt64(-10),
|
||||
exp: "-1E+1",
|
||||
"3 decimal places": {
|
||||
x: must(NewDecFromString("0.001")),
|
||||
exp: "0.001",
|
||||
},
|
||||
"9 with trailing zeros": {
|
||||
x: must(NewDecFromString("9." + strings.Repeat("0", 34))),
|
||||
exp: "9E+0",
|
||||
},
|
||||
"negative 1 with negative exponent zeros": {
|
||||
x: must(NewDecFromString("-1.000001")),
|
||||
exp: "-1.000001E+0",
|
||||
},
|
||||
"negative 1 with trailing zeros": {
|
||||
x: must(NewDecFromString("-1." + strings.Repeat("0", 34))),
|
||||
exp: "-1E+0",
|
||||
"4 decimal places": {
|
||||
x: must(NewDecFromString("0.0001")),
|
||||
exp: "0.0001",
|
||||
},
|
||||
"5 decimal places": {
|
||||
x: must(NewDecFromString("0.00001")),
|
||||
exp: "1E-5",
|
||||
exp: "0.00001",
|
||||
},
|
||||
"6 decimal places": {
|
||||
x: must(NewDecFromString("0.000001")),
|
||||
@ -1367,17 +1351,73 @@ func TestMarshalUnmarshal(t *testing.T) {
|
||||
x: must(NewDecFromString("0.0000001")),
|
||||
exp: "1E-7",
|
||||
},
|
||||
"1": {
|
||||
x: must(NewDecFromString("1")),
|
||||
exp: "1",
|
||||
},
|
||||
"12": {
|
||||
x: must(NewDecFromString("12")),
|
||||
exp: "12",
|
||||
},
|
||||
"123": {
|
||||
x: must(NewDecFromString("123")),
|
||||
exp: "123",
|
||||
},
|
||||
"1234": {
|
||||
x: must(NewDecFromString("1234")),
|
||||
exp: "1234",
|
||||
},
|
||||
"12345": {
|
||||
x: must(NewDecFromString("12345")),
|
||||
exp: "12345",
|
||||
},
|
||||
"123456": {
|
||||
x: must(NewDecFromString("123456")),
|
||||
exp: "123456",
|
||||
},
|
||||
"1234567": {
|
||||
x: must(NewDecFromString("1234567")),
|
||||
exp: "1.234567E+6",
|
||||
},
|
||||
"12345678": {
|
||||
x: must(NewDecFromString("12345678")),
|
||||
exp: "12.345678E+6",
|
||||
},
|
||||
"123456789": {
|
||||
x: must(NewDecFromString("123456789")),
|
||||
exp: "123.456789E+6",
|
||||
},
|
||||
"1234567890": {
|
||||
x: must(NewDecFromString("1234567890")),
|
||||
exp: "123.456789E+7",
|
||||
},
|
||||
"12345678900": {
|
||||
x: must(NewDecFromString("12345678900")),
|
||||
exp: "123.456789E+8",
|
||||
},
|
||||
"negative 1 with negative exponent": {
|
||||
x: must(NewDecFromString("-1.000001")),
|
||||
exp: "-1.000001",
|
||||
},
|
||||
"-1.0000001 - negative 1 with negative exponent": {
|
||||
x: must(NewDecFromString("-1.0000001")),
|
||||
exp: "-1.0000001",
|
||||
},
|
||||
"3 decimal places before the comma": {
|
||||
x: must(NewDecFromString("100")),
|
||||
exp: "100",
|
||||
},
|
||||
"4 decimal places before the comma": {
|
||||
x: must(NewDecFromString("1000")),
|
||||
exp: "1E+3",
|
||||
exp: "1000",
|
||||
},
|
||||
"5 decimal places before the comma": {
|
||||
x: must(NewDecFromString("10000")),
|
||||
exp: "1E+4",
|
||||
exp: "10000",
|
||||
},
|
||||
"6 decimal places before the comma": {
|
||||
x: must(NewDecFromString("100000")),
|
||||
exp: "1E+5",
|
||||
exp: "100000",
|
||||
},
|
||||
"7 decimal places before the comma": {
|
||||
x: must(NewDecFromString("1000000")),
|
||||
@ -1388,12 +1428,12 @@ func TestMarshalUnmarshal(t *testing.T) {
|
||||
exp: "1E+100000",
|
||||
},
|
||||
"1.1e100000": {
|
||||
x: NewDecWithExp(11, 100_000),
|
||||
expErr: ErrInvalidDec,
|
||||
x: must(NewDecFromString("1.1e100000")),
|
||||
exp: "1.1E+100000",
|
||||
},
|
||||
"1.e100000": {
|
||||
x: NewDecWithExp(1, 100_000),
|
||||
exp: "1E+100000",
|
||||
"1e100001": {
|
||||
x: NewDecWithExp(1, 100_001),
|
||||
expErr: ErrInvalidDec,
|
||||
},
|
||||
}
|
||||
for name, spec := range specs {
|
||||
@ -1404,9 +1444,12 @@ func TestMarshalUnmarshal(t *testing.T) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, gotErr)
|
||||
unmarshalled := new(Dec)
|
||||
require.NoError(t, unmarshalled.Unmarshal(marshaled))
|
||||
assert.Equal(t, spec.exp, unmarshalled.dec.Text('E'))
|
||||
assert.Equal(t, spec.exp, string(marshaled))
|
||||
// and backwards
|
||||
unmarshalledDec := new(Dec)
|
||||
require.NoError(t, unmarshalledDec.Unmarshal(marshaled))
|
||||
assert.Equal(t, spec.exp, unmarshalledDec.String())
|
||||
assert.True(t, spec.x.Equal(*unmarshalledDec))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user