From 4d8a2ace9329b0a9c62273425b32c49d0d75ce77 Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 18 Apr 2024 10:26:02 +0200 Subject: [PATCH 01/22] update dec w benchmark testing --- math/dec.go | 1073 ++++++--------------------------- math/dec_bench_test.go | 98 +++ math/dec_legacy.go | 969 ++++++++++++++++++++++++++++++ math/dec_legacy_test.go | 784 ++++++++++++++++++++++++ math/dec_test.go | 1259 ++++++++++++++++++--------------------- math/go.mod | 36 +- math/go.sum | 68 +++ math/math.go | 73 +++ 8 files changed, 2812 insertions(+), 1548 deletions(-) create mode 100644 math/dec_bench_test.go create mode 100644 math/dec_legacy.go create mode 100644 math/dec_legacy_test.go create mode 100644 math/math.go diff --git a/math/dec.go b/math/dec.go index 0ca1cfcb8c22..2736bf67a60f 100644 --- a/math/dec.go +++ b/math/dec.go @@ -1,969 +1,288 @@ package math import ( - "encoding/json" - "errors" "fmt" "math/big" - "strconv" - "strings" - "testing" + + "cosmossdk.io/errors" + "github.com/cockroachdb/apd/v2" ) -// LegacyDec NOTE: never use new(Dec) or else we will panic unmarshalling into the -// nil embedded big.Int -type LegacyDec struct { - i *big.Int +// Dec is a wrapper struct around apd.Decimal that does no mutation of apd.Decimal's when performing +// arithmetic, instead creating a new apd.Decimal for every operation ensuring usage is safe. +// +// Using apd.Decimal directly can be unsafe because apd operations mutate the underlying Decimal, +// but when copying the big.Int structure can be shared between Decimal instances causing corruption. +// This was originally discovered in regen0-network/mainnet#15. +type Dec struct { + dec apd.Decimal } +// constants for more convenient intent behind dec.Cmp values. const ( - // LegacyPrecision number of decimal places - LegacyPrecision = 18 - - // LegacyDecimalPrecisionBits bits required to represent the above precision - // Ceiling[Log2[10^Precision - 1]] - LegacyDecimalPrecisionBits = 60 - - // decimalTruncateBits is the minimum number of bits removed - // by a truncate operation. It is equal to - // Floor[Log2[10^Precision - 1]]. - decimalTruncateBits = LegacyDecimalPrecisionBits - 1 - - maxDecBitLen = MaxBitLen + decimalTruncateBits - - // maxApproxRootIterations max number of iterations in ApproxRoot function - maxApproxRootIterations = 300 + GreaterThan = 1 + LessThan = -1 + EqualTo = 0 ) -var ( - precisionReuse = new(big.Int).Exp(big.NewInt(10), big.NewInt(LegacyPrecision), nil) - fivePrecision = new(big.Int).Quo(precisionReuse, big.NewInt(2)) - precisionMultipliers []*big.Int - zeroInt = big.NewInt(0) - oneInt = big.NewInt(1) - tenInt = big.NewInt(10) - smallestDec = LegacySmallestDec() -) +const mathCodespace = "math" -// Decimal errors var ( - ErrLegacyEmptyDecimalStr = errors.New("decimal string cannot be empty") - ErrLegacyInvalidDecimalLength = errors.New("invalid decimal length") - ErrLegacyInvalidDecimalStr = errors.New("invalid decimal string") + ErrInvalidDecString = errors.Register(mathCodespace, 1, "invalid decimal string") + ErrUnexpectedRounding = errors.Register(mathCodespace, 2, "unexpected rounding") + ErrNonIntegeral = errors.Register(mathCodespace, 3, "value is non-integral") + ErrInfiniteString = errors.Register(mathCodespace, 4, "value is infinite") ) -// Set precision multipliers -func init() { - precisionMultipliers = make([]*big.Int, LegacyPrecision+1) - for i := 0; i <= LegacyPrecision; i++ { - precisionMultipliers[i] = calcPrecisionMultiplier(int64(i)) - } -} - -func precisionInt() *big.Int { - return new(big.Int).Set(precisionReuse) -} - -func LegacyZeroDec() LegacyDec { return LegacyDec{new(big.Int).Set(zeroInt)} } -func LegacyOneDec() LegacyDec { return LegacyDec{precisionInt()} } -func LegacySmallestDec() LegacyDec { return LegacyDec{new(big.Int).Set(oneInt)} } - -// calculate the precision multiplier -func calcPrecisionMultiplier(prec int64) *big.Int { - if prec < 0 { - panic(fmt.Sprintf("negative precision %v", prec)) - } - - if prec > LegacyPrecision { - panic(fmt.Sprintf("too much precision, maximum %v, provided %v", LegacyPrecision, prec)) - } - zerosToAdd := LegacyPrecision - prec - multiplier := new(big.Int).Exp(tenInt, big.NewInt(zerosToAdd), nil) - return multiplier -} - -// get the precision multiplier, do not mutate result -func precisionMultiplier(prec int64) *big.Int { - if prec < 0 { - panic(fmt.Sprintf("negative precision %v", prec)) - } - - if prec > LegacyPrecision { - panic(fmt.Sprintf("too much precision, maximum %v, provided %v", LegacyPrecision, prec)) - } - return precisionMultipliers[prec] -} - -// LegacyNewDec create a new Dec from integer assuming whole number -func LegacyNewDec(i int64) LegacyDec { - return LegacyNewDecWithPrec(i, 0) -} - -// LegacyNewDecWithPrec create a new Dec from integer with decimal place at prec -// CONTRACT: prec <= Precision -func LegacyNewDecWithPrec(i, prec int64) LegacyDec { - bi := big.NewInt(i) - return LegacyDec{ - bi.Mul(bi, precisionMultiplier(prec)), - } -} - -// LegacyNewDecFromBigInt create a new Dec from big integer assuming whole numbers -// CONTRACT: prec <= Precision -func LegacyNewDecFromBigInt(i *big.Int) LegacyDec { - return LegacyNewDecFromBigIntWithPrec(i, 0) -} - -// LegacyNewDecFromBigIntWithPrec create a new Dec from big integer assuming whole numbers -// CONTRACT: prec <= Precision -func LegacyNewDecFromBigIntWithPrec(i *big.Int, prec int64) LegacyDec { - return LegacyDec{ - new(big.Int).Mul(i, precisionMultiplier(prec)), - } -} - -// LegacyNewDecFromInt create a new Dec from big integer assuming whole numbers -// CONTRACT: prec <= Precision -func LegacyNewDecFromInt(i Int) LegacyDec { - return LegacyNewDecFromIntWithPrec(i, 0) +// In cosmos-sdk#7773, decimal128 (with 34 digits of precision) was suggested for performing +// Quo/Mult arithmetic generically across the SDK. Even though the SDK +// has yet to support a GDA with decimal128 (34 digits), we choose to utilize it here. +// https://github.com/cosmos/cosmos-sdk/issues/7773#issuecomment-725006142 +var dec128Context = apd.Context{ + Precision: 34, + MaxExponent: apd.MaxExponent, + MinExponent: apd.MinExponent, + Traps: apd.DefaultTraps, } -// LegacyNewDecFromIntWithPrec create a new Dec from big integer with decimal place at prec -// CONTRACT: prec <= Precision -func LegacyNewDecFromIntWithPrec(i Int, prec int64) LegacyDec { - return LegacyDec{ - new(big.Int).Mul(i.BigIntMut(), precisionMultiplier(prec)), +func NewDecFromString(s string) (Dec, error) { + if s == "" { + s = "0" } -} - -// LegacyNewDecFromStr create a decimal from an input decimal string. -// valid must come in the form: -// -// (-) whole integers (.) decimal integers -// -// examples of acceptable input include: -// -// -123.456 -// 456.7890 -// 345 -// -456789 -// -// NOTE - An error will return if more decimal places -// are provided in the string than the constant Precision. -// -// CONTRACT - This function does not mutate the input str. -func LegacyNewDecFromStr(str string) (LegacyDec, error) { - // first extract any negative symbol - neg := false - if len(str) > 0 && str[0] == '-' { - neg = true - str = str[1:] - } - - if len(str) == 0 { - return LegacyDec{}, ErrLegacyEmptyDecimalStr - } - - strs := strings.Split(str, ".") - lenDecs := 0 - combinedStr := strs[0] - - if len(strs) == 2 { // has a decimal place - lenDecs = len(strs[1]) - if lenDecs == 0 || len(combinedStr) == 0 { - return LegacyDec{}, ErrLegacyInvalidDecimalLength - } - combinedStr += strs[1] - } else if len(strs) > 2 { - return LegacyDec{}, ErrLegacyInvalidDecimalStr - } - - if lenDecs > LegacyPrecision { - return LegacyDec{}, fmt.Errorf("value '%s' exceeds max precision by %d decimal places: max precision %d", str, LegacyPrecision-lenDecs, LegacyPrecision) - } - - // add some extra zero's to correct to the Precision factor - zerosToAdd := LegacyPrecision - lenDecs - zeros := strings.Repeat("0", zerosToAdd) - combinedStr += zeros - - combined, ok := new(big.Int).SetString(combinedStr, 10) // base 10 - if !ok { - return LegacyDec{}, fmt.Errorf("failed to set decimal string with base 10: %s", combinedStr) - } - if combined.BitLen() > maxDecBitLen { - return LegacyDec{}, fmt.Errorf("decimal '%s' out of range; bitLen: got %d, max %d", str, combined.BitLen(), maxDecBitLen) - } - if neg { - combined = new(big.Int).Neg(combined) - } - - return LegacyDec{combined}, nil -} - -// LegacyMustNewDecFromStr Decimal from string, panic on error -func LegacyMustNewDecFromStr(s string) LegacyDec { - dec, err := LegacyNewDecFromStr(s) + d, _, err := apd.NewFromString(s) if err != nil { - panic(err) + return Dec{}, ErrInvalidDecString.Wrap(err.Error()) } - return dec -} - -func (d LegacyDec) IsNil() bool { return d.i == nil } // is decimal nil -func (d LegacyDec) IsZero() bool { return (d.i).Sign() == 0 } // is equal to zero -func (d LegacyDec) IsNegative() bool { return (d.i).Sign() == -1 } // is negative -func (d LegacyDec) IsPositive() bool { return (d.i).Sign() == 1 } // is positive -func (d LegacyDec) Equal(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) == 0 } // equal decimals -func (d LegacyDec) GT(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) > 0 } // greater than -func (d LegacyDec) GTE(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) >= 0 } // greater than or equal -func (d LegacyDec) LT(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) < 0 } // less than -func (d LegacyDec) LTE(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) <= 0 } // less than or equal -func (d LegacyDec) Neg() LegacyDec { return LegacyDec{new(big.Int).Neg(d.i)} } // reverse the decimal sign -func (d LegacyDec) NegMut() LegacyDec { d.i.Neg(d.i); return d } // reverse the decimal sign, mutable -func (d LegacyDec) Abs() LegacyDec { return LegacyDec{new(big.Int).Abs(d.i)} } // absolute value -func (d LegacyDec) AbsMut() LegacyDec { d.i.Abs(d.i); return d } // absolute value, mutable -func (d LegacyDec) Set(d2 LegacyDec) LegacyDec { d.i.Set(d2.i); return d } // set to existing dec value -func (d LegacyDec) Clone() LegacyDec { return LegacyDec{new(big.Int).Set(d.i)} } // clone new dec - -// BigInt returns a copy of the underlying big.Int. -func (d LegacyDec) BigInt() *big.Int { - if d.IsNil() { - return nil - } - - cp := new(big.Int) - return cp.Set(d.i) -} -// BigIntMut converts LegacyDec to big.Int, mutative the input -func (d LegacyDec) BigIntMut() *big.Int { - if d.IsNil() { - return nil + d1 := Dec{*d} + if d1.dec.Form == apd.Infinite { + return d1, ErrInfiniteString.Wrapf(s) } - return d.i -} - -func (d LegacyDec) ImmutOp(op func(LegacyDec, LegacyDec) LegacyDec, d2 LegacyDec) LegacyDec { - return op(d.Clone(), d2) + return d1, nil } -func (d LegacyDec) ImmutOpInt(op func(LegacyDec, Int) LegacyDec, d2 Int) LegacyDec { - return op(d.Clone(), d2) -} - -func (d LegacyDec) ImmutOpInt64(op func(LegacyDec, int64) LegacyDec, d2 int64) LegacyDec { - // TODO: use already allocated operand bigint to avoid - // newint each time, add mutex for race condition - // Issue: https://github.com/cosmos/cosmos-sdk/issues/11166 - return op(d.Clone(), d2) -} - -func (d LegacyDec) SetInt64(i int64) LegacyDec { - d.i.SetInt64(i) - d.i.Mul(d.i, precisionReuse) - return d -} - -// Add addition -func (d LegacyDec) Add(d2 LegacyDec) LegacyDec { - return d.ImmutOp(LegacyDec.AddMut, d2) -} - -// AddMut mutable addition -func (d LegacyDec) AddMut(d2 LegacyDec) LegacyDec { - d.i.Add(d.i, d2.i) - - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } - return d -} - -// Sub subtraction -func (d LegacyDec) Sub(d2 LegacyDec) LegacyDec { - return d.ImmutOp(LegacyDec.SubMut, d2) -} - -// SubMut mutable subtraction -func (d LegacyDec) SubMut(d2 LegacyDec) LegacyDec { - d.i.Sub(d.i, d2.i) - - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } - return d -} - -// Mul multiplication -func (d LegacyDec) Mul(d2 LegacyDec) LegacyDec { - return d.ImmutOp(LegacyDec.MulMut, d2) -} - -// MulMut mutable multiplication -func (d LegacyDec) MulMut(d2 LegacyDec) LegacyDec { - d.i.Mul(d.i, d2.i) - chopped := chopPrecisionAndRound(d.i) - - if chopped.BitLen() > maxDecBitLen { - panic("Int overflow") - } - *d.i = *chopped - return d -} - -// MulTruncate multiplication truncate -func (d LegacyDec) MulTruncate(d2 LegacyDec) LegacyDec { - return d.ImmutOp(LegacyDec.MulTruncateMut, d2) -} - -// MulTruncateMut mutable multiplication truncate -func (d LegacyDec) MulTruncateMut(d2 LegacyDec) LegacyDec { - d.i.Mul(d.i, d2.i) - chopPrecisionAndTruncate(d.i) - - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } - return d -} - -// MulRoundUp multiplication round up at precision end. -func (d LegacyDec) MulRoundUp(d2 LegacyDec) LegacyDec { - return d.ImmutOp(LegacyDec.MulRoundUpMut, d2) -} - -// MulRoundUpMut mutable multiplication with round up at precision end. -func (d LegacyDec) MulRoundUpMut(d2 LegacyDec) LegacyDec { - d.i.Mul(d.i, d2.i) - chopPrecisionAndRoundUp(d.i) - - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } - return d -} - -// MulInt multiplication -func (d LegacyDec) MulInt(i Int) LegacyDec { - return d.ImmutOpInt(LegacyDec.MulIntMut, i) -} - -func (d LegacyDec) MulIntMut(i Int) LegacyDec { - d.i.Mul(d.i, i.BigIntMut()) - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } - return d -} - -// MulInt64 multiplication with int64 -func (d LegacyDec) MulInt64(i int64) LegacyDec { - return d.ImmutOpInt64(LegacyDec.MulInt64Mut, i) -} - -func (d LegacyDec) MulInt64Mut(i int64) LegacyDec { - d.i.Mul(d.i, big.NewInt(i)) - - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } - return d -} - -// Quo quotient -func (d LegacyDec) Quo(d2 LegacyDec) LegacyDec { - return d.ImmutOp(LegacyDec.QuoMut, d2) -} - -var squaredPrecisionReuse = new(big.Int).Mul(precisionReuse, precisionReuse) - -// QuoMut mutable quotient -func (d LegacyDec) QuoMut(d2 LegacyDec) LegacyDec { - // multiply by precision twice - d.i.Mul(d.i, squaredPrecisionReuse) - d.i.Quo(d.i, d2.i) - - chopPrecisionAndRound(d.i) - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } - return d -} - -// QuoTruncate quotient truncate -func (d LegacyDec) QuoTruncate(d2 LegacyDec) LegacyDec { - return d.ImmutOp(LegacyDec.QuoTruncateMut, d2) -} - -// QuoTruncateMut mutable quotient truncate -func (d LegacyDec) QuoTruncateMut(d2 LegacyDec) LegacyDec { - // multiply precision twice - d.i.Mul(d.i, squaredPrecisionReuse) - d.i.Quo(d.i, d2.i) - - chopPrecisionAndTruncate(d.i) - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } - return d -} - -// QuoRoundUp quotient, round up -func (d LegacyDec) QuoRoundUp(d2 LegacyDec) LegacyDec { - return d.ImmutOp(LegacyDec.QuoRoundupMut, d2) -} - -// QuoRoundupMut mutable quotient, round up -func (d LegacyDec) QuoRoundupMut(d2 LegacyDec) LegacyDec { - // multiply precision twice - d.i.Mul(d.i, squaredPrecisionReuse) - d.i.Quo(d.i, d2.i) - - chopPrecisionAndRoundUp(d.i) - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") +func NewNonNegativeDecFromString(s string) (Dec, error) { + d, err := NewDecFromString(s) + if err != nil { + return Dec{}, ErrInvalidDecString.Wrap(err.Error()) } - return d -} - -// QuoInt quotient -func (d LegacyDec) QuoInt(i Int) LegacyDec { - return d.ImmutOpInt(LegacyDec.QuoIntMut, i) -} - -func (d LegacyDec) QuoIntMut(i Int) LegacyDec { - d.i.Quo(d.i, i.BigIntMut()) - return d -} - -// QuoInt64 quotient with int64 -func (d LegacyDec) QuoInt64(i int64) LegacyDec { - return d.ImmutOpInt64(LegacyDec.QuoInt64Mut, i) -} - -func (d LegacyDec) QuoInt64Mut(i int64) LegacyDec { - d.i.Quo(d.i, big.NewInt(i)) - return d -} - -// ApproxRoot returns an approximate estimation of a Dec's positive real nth root -// using Newton's method (where n is positive). The algorithm starts with some guess and -// computes the sequence of improved guesses until an answer converges to an -// approximate answer. It returns `|d|.ApproxRoot() * -1` if input is negative. -// A maximum number of 100 iterations is used a backup boundary condition for -// cases where the answer never converges enough to satisfy the main condition. -func (d LegacyDec) ApproxRoot(root uint64) (guess LegacyDec, err error) { - defer func() { - if r := recover(); r != nil { - var ok bool - err, ok = r.(error) - if !ok { - err = errors.New("out of bounds") - } - } - }() - if d.IsNegative() { - absRoot, err := d.Neg().ApproxRoot(root) - return absRoot.NegMut(), err + return Dec{}, ErrInvalidDecString.Wrapf("expected a non-negative decimal, got %s", s) } - - // One decimal, that we invalidate later. Helps us save a heap allocation. - scratchOneDec := LegacyOneDec() - if root == 1 || d.IsZero() || d.Equal(scratchOneDec) { - return d, nil - } - - if root == 0 { - return scratchOneDec, nil - } - - guess, delta := scratchOneDec, LegacyOneDec() - - for iter := 0; iter < maxApproxRootIterations && delta.Abs().GT(smallestDec); iter++ { - prev := guess.Power(root - 1) - if prev.IsZero() { - prev = smallestDec - } - delta.Set(d).QuoMut(prev) - delta.SubMut(guess) - delta.QuoInt64Mut(int64(root)) - - guess.AddMut(delta) - } - - return guess, nil -} - -// Power returns a the result of raising to a positive integer power -func (d LegacyDec) Power(power uint64) LegacyDec { - res := LegacyDec{new(big.Int).Set(d.i)} - return res.PowerMut(power) + return d, nil } -func (d LegacyDec) PowerMut(power uint64) LegacyDec { - if power == 0 { - // Set to 1 with the correct precision. - d.i.Set(precisionReuse) - return d +func NewNonNegativeFixedDecFromString(s string, max uint32) (Dec, error) { + d, err := NewNonNegativeDecFromString(s) + if err != nil { + return Dec{}, err } - tmp := LegacyOneDec() - - for i := power; i > 1; { - if i%2 != 0 { - tmp.MulMut(d) - } - i /= 2 - d.MulMut(d) + if d.NumDecimalPlaces() > max { + return Dec{}, fmt.Errorf("%s exceeds maximum decimal places: %d", s, max) } - - return d.MulMut(tmp) + return d, nil } -// ApproxSqrt is a wrapper around ApproxRoot for the common special case -// of finding the square root of a number. It returns -(sqrt(abs(d)) if input is negative. -func (d LegacyDec) ApproxSqrt() (LegacyDec, error) { - return d.ApproxRoot(2) -} - -// IsInteger is integer, e.g. decimals are zero -func (d LegacyDec) IsInteger() bool { - return new(big.Int).Rem(d.i, precisionReuse).Sign() == 0 -} - -// Format format decimal state -func (d LegacyDec) Format(s fmt.State, verb rune) { - _, err := s.Write([]byte(d.String())) +func NewPositiveDecFromString(s string) (Dec, error) { + d, err := NewDecFromString(s) if err != nil { - panic(err) + return Dec{}, ErrInvalidDecString.Wrap(err.Error()) } -} - -func (d LegacyDec) String() string { - if d.i == nil { - return d.i.String() - } - - isNeg := d.IsNegative() - - if isNeg { - d = d.Neg() + if !d.IsPositive() || !d.IsFinite() { + return Dec{}, ErrInvalidDecString.Wrapf("expected a positive decimal, got %s", s) } + return d, nil +} - bzInt, err := d.i.MarshalText() +func NewPositiveFixedDecFromString(s string, max uint32) (Dec, error) { + d, err := NewPositiveDecFromString(s) if err != nil { - return "" + return Dec{}, err } - inputSize := len(bzInt) - - var bzStr []byte - - // TODO: Remove trailing zeros - // case 1, purely decimal - if inputSize <= LegacyPrecision { - bzStr = make([]byte, LegacyPrecision+2) - - // 0. prefix - bzStr[0] = byte('0') - bzStr[1] = byte('.') - - // set relevant digits to 0 - for i := 0; i < LegacyPrecision-inputSize; i++ { - bzStr[i+2] = byte('0') - } - - // set final digits - copy(bzStr[2+(LegacyPrecision-inputSize):], bzInt) - } else { - // inputSize + 1 to account for the decimal point that is being added - bzStr = make([]byte, inputSize+1) - decPointPlace := inputSize - LegacyPrecision - - copy(bzStr, bzInt[:decPointPlace]) // pre-decimal digits - bzStr[decPointPlace] = byte('.') // decimal point - copy(bzStr[decPointPlace+1:], bzInt[decPointPlace:]) // post-decimal digits + if d.NumDecimalPlaces() > max { + return Dec{}, fmt.Errorf("%s exceeds maximum decimal places: %d", s, max) } - - if isNeg { - return "-" + string(bzStr) - } - - return string(bzStr) + return d, nil } -// Float64 returns the float64 representation of a Dec. -// Will return the error if the conversion failed. -func (d LegacyDec) Float64() (float64, error) { - return strconv.ParseFloat(d.String(), 64) +func NewDecFromInt64(x int64) Dec { + var res Dec + res.dec.SetInt64(x) + return res } -// MustFloat64 returns the float64 representation of a Dec. -// Would panic if the conversion failed. -func (d LegacyDec) MustFloat64() float64 { - if value, err := strconv.ParseFloat(d.String(), 64); err != nil { - panic(err) - } else { - return value - } +// NewDecFinite returns a decimal with a value of coeff * 10^exp. +func NewDecFinite(coeff int64, exp int32) Dec { + var res Dec + res.dec.SetFinite(coeff, exp) + return res } -// ____ -// __| |__ "chop 'em -// ` \ round!" -// ___|| ~ _ -bankers -// | | __ -// | | | __|__|__ -// |_____: / | $$$ | -// |________| - -// Remove a Precision amount of rightmost digits and perform bankers rounding -// on the remainder (gaussian rounding) on the digits which have been removed. -// -// Mutates the input. Use the non-mutative version if that is undesired -func chopPrecisionAndRound(d *big.Int) *big.Int { - // remove the negative and add it back when returning - if d.Sign() == -1 { - // make d positive, compute chopped value, and then un-mutate d - d = d.Neg(d) - d = chopPrecisionAndRound(d) - d = d.Neg(d) - return d - } - - // get the truncated quotient and remainder - quo, rem := d, big.NewInt(0) - quo, rem = quo.QuoRem(d, precisionReuse, rem) - - if rem.Sign() == 0 { // remainder is zero - return quo - } - - switch rem.Cmp(fivePrecision) { - case -1: - return quo - case 1: - return quo.Add(quo, oneInt) - default: // bankers rounding must take place - // always round to an even number - if quo.Bit(0) == 0 { - return quo - } - return quo.Add(quo, oneInt) - } +// Add returns a new Dec with value `x+y` without mutating any argument and error if +// there is an overflow. +func (x Dec) Add(y Dec) (Dec, error) { + var z Dec + _, err := apd.BaseContext.Add(&z.dec, &x.dec, &y.dec) + return z, errors.Wrap(err, "decimal addition error") } -func chopPrecisionAndRoundUp(d *big.Int) *big.Int { - // remove the negative and add it back when returning - if d.Sign() == -1 { - // make d positive, compute chopped value, and then un-mutate d - d = d.Neg(d) - // truncate since d is negative... - chopPrecisionAndTruncate(d) - d = d.Neg(d) - return d - } - - // get the truncated quotient and remainder - quo, rem := d, big.NewInt(0) - quo, rem = quo.QuoRem(d, precisionReuse, rem) - - if rem.Sign() == 0 { // remainder is zero - return quo - } - - return quo.Add(quo, oneInt) +// Sub returns a new Dec with value `x-y` without mutating any argument and error if +// there is an overflow. +func (x Dec) Sub(y Dec) (Dec, error) { + var z Dec + _, err := apd.BaseContext.Sub(&z.dec, &x.dec, &y.dec) + return z, errors.Wrap(err, "decimal subtraction error") } -func chopPrecisionAndRoundNonMutative(d *big.Int) *big.Int { - tmp := new(big.Int).Set(d) - return chopPrecisionAndRound(tmp) +// Quo returns a new Dec with value `x/y` (formatted as decimal128, 34 digit precision) without mutating any +// argument and error if there is an overflow. +func (x Dec) Quo(y Dec) (Dec, error) { + var z Dec + _, err := dec128Context.Quo(&z.dec, &x.dec, &y.dec) + return z, errors.Wrap(err, "decimal quotient error") } -// RoundInt64 rounds the decimal using bankers rounding -func (d LegacyDec) RoundInt64() int64 { - chopped := chopPrecisionAndRoundNonMutative(d.i) - if !chopped.IsInt64() { - panic("Int64() out of bound") +// MulExact returns a new dec with value x * y. The product must not round or +// ErrUnexpectedRounding will be returned. +func (x Dec) MulExact(y Dec) (Dec, error) { + var z Dec + condition, err := dec128Context.Mul(&z.dec, &x.dec, &y.dec) + if err != nil { + return z, err } - return chopped.Int64() -} - -// RoundInt round the decimal using bankers rounding -func (d LegacyDec) RoundInt() Int { - return NewIntFromBigIntMut(chopPrecisionAndRoundNonMutative(d.i)) -} - -// chopPrecisionAndTruncate is similar to chopPrecisionAndRound, -// but always rounds down. It does not mutate the input. -func chopPrecisionAndTruncate(d *big.Int) { - d.Quo(d, precisionReuse) -} - -func chopPrecisionAndTruncateNonMutative(d *big.Int) *big.Int { - tmp := new(big.Int).Set(d) - chopPrecisionAndTruncate(tmp) - return tmp -} - -// TruncateInt64 truncates the decimals from the number and returns an int64 -func (d LegacyDec) TruncateInt64() int64 { - chopped := chopPrecisionAndTruncateNonMutative(d.i) - if !chopped.IsInt64() { - panic("Int64() out of bound") + if condition.Rounded() { + return z, ErrUnexpectedRounding } - return chopped.Int64() + return z, nil } -// TruncateInt truncates the decimals from the number and returns an Int -func (d LegacyDec) TruncateInt() Int { - return NewIntFromBigIntMut(chopPrecisionAndTruncateNonMutative(d.i)) -} - -// TruncateDec truncates the decimals from the number and returns a Dec -func (d LegacyDec) TruncateDec() LegacyDec { - return LegacyNewDecFromBigInt(chopPrecisionAndTruncateNonMutative(d.i)) -} - -// Ceil returns the smallest integer value (as a decimal) that is greater than -// or equal to the given decimal. -func (d LegacyDec) Ceil() LegacyDec { - tmp := new(big.Int).Set(d.i) - - quo, rem := tmp, big.NewInt(0) - quo, rem = quo.QuoRem(tmp, precisionReuse, rem) - - // no need to round with a zero remainder regardless of sign - if rem.Sign() == 0 { - return LegacyNewDecFromBigInt(quo) - } else if rem.Sign() == -1 { - return LegacyNewDecFromBigInt(quo) +// QuoExact is a version of Quo that returns ErrUnexpectedRounding if any rounding occurred. +func (x Dec) QuoExact(y Dec) (Dec, error) { + var z Dec + condition, err := dec128Context.Quo(&z.dec, &x.dec, &y.dec) + if err != nil { + return z, err } - - if d.i.BitLen() >= maxDecBitLen { - panic("Int overflow") + if condition.Rounded() { + return z, ErrUnexpectedRounding } - - return LegacyNewDecFromBigInt(quo.Add(quo, oneInt)) + return z, errors.Wrap(err, "decimal quotient error") } -// LegacyMaxSortableDec is the largest Dec that can be passed into SortableDecBytes() -// Its negative form is the least Dec that can be passed in. -var LegacyMaxSortableDec LegacyDec - -func init() { - LegacyMaxSortableDec = LegacyOneDec().Quo(LegacySmallestDec()) +// QuoInteger returns a new integral Dec with value `x/y` (formatted as decimal128, with 34 digit precision) +// without mutating any argument and error if there is an overflow. +func (x Dec) QuoInteger(y Dec) (Dec, error) { + var z Dec + _, err := dec128Context.QuoInteger(&z.dec, &x.dec, &y.dec) + return z, errors.Wrap(err, "decimal quotient error") } -// LegacyValidSortableDec ensures that a Dec is within the sortable bounds, -// a Dec can't have a precision of less than 10^-18. -// Max sortable decimal was set to the reciprocal of SmallestDec. -func LegacyValidSortableDec(dec LegacyDec) bool { - return dec.Abs().LTE(LegacyMaxSortableDec) +// Rem returns the integral remainder from `x/y` (formatted as decimal128, with 34 digit precision) without +// mutating any argument and error if the integer part of x/y cannot fit in 34 digit precision +func (x Dec) Rem(y Dec) (Dec, error) { + var z Dec + _, err := dec128Context.Rem(&z.dec, &x.dec, &y.dec) + return z, errors.Wrap(err, "decimal remainder error") } -// LegacySortableDecBytes returns a byte slice representation of a Dec that can be sorted. -// Left and right pads with 0s so there are 18 digits to left and right of the decimal point. -// For this reason, there is a maximum and minimum value for this, enforced by ValidSortableDec. -func LegacySortableDecBytes(dec LegacyDec) []byte { - if !LegacyValidSortableDec(dec) { - panic("dec must be within bounds") - } - // Instead of adding an extra byte to all sortable decs in order to handle max sortable, we just - // makes its bytes be "max" which comes after all numbers in ASCIIbetical order - if dec.Equal(LegacyMaxSortableDec) { - return []byte("max") - } - // For the same reason, we make the bytes of minimum sortable dec be --, which comes before all numbers. - if dec.Equal(LegacyMaxSortableDec.Neg()) { - return []byte("--") - } - // We move the negative sign to the front of all the left padded 0s, to make negative numbers come before positive numbers - if dec.IsNegative() { - return append([]byte("-"), []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", LegacyPrecision*2+1), dec.Abs().String()))...) - } - return []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", LegacyPrecision*2+1), dec.String())) +// Mul returns a new Dec with value `x*y` (formatted as decimal128, with 34 digit precision) without +// mutating any argument and error if there is an overflow. +func (x Dec) Mul(y Dec) (Dec, error) { + var z Dec + _, err := dec128Context.Mul(&z.dec, &x.dec, &y.dec) + return z, errors.Wrap(err, "decimal multiplication error") } -// reuse nil values -var nilJSON []byte - -func init() { - empty := new(big.Int) - bz, _ := empty.MarshalText() - nilJSON, _ = json.Marshal(string(bz)) +// Int64 converts x to an int64 or returns an error if x cannot +// fit precisely into an int64. +func (x Dec) Int64() (int64, error) { + return x.dec.Int64() } -// MarshalJSON marshals the decimal -func (d LegacyDec) MarshalJSON() ([]byte, error) { - if d.i == nil { - return nilJSON, nil - } - return json.Marshal(d.String()) -} - -// UnmarshalJSON defines custom decoding scheme -func (d *LegacyDec) UnmarshalJSON(bz []byte) error { - if d.i == nil { - d.i = new(big.Int) - } - - var text string - err := json.Unmarshal(bz, &text) - if err != nil { - return err +// BigInt converts x to a *big.Int or returns an error if x cannot +// fit precisely into an *big.Int. +func (x Dec) BigInt() (*big.Int, error) { + y, _ := x.Reduce() + z := &big.Int{} + z, ok := z.SetString(y.String(), 10) + if !ok { + return nil, ErrNonIntegeral + } + return z, nil +} + +// SdkIntTrim rounds decimal number to the integer towards zero and converts it to `sdkmath.Int`. +// Panics if x is bigger the SDK Int max value +func (x Dec) SdkIntTrim() Int { + y, _ := x.Reduce() + var r = y.dec.Coeff + if y.dec.Exponent != 0 { + decs := big.NewInt(10) + if y.dec.Exponent > 0 { + decs.Exp(decs, big.NewInt(int64(y.dec.Exponent)), nil) + r.Mul(&y.dec.Coeff, decs) + } else { + decs.Exp(decs, big.NewInt(int64(-y.dec.Exponent)), nil) + r.Quo(&y.dec.Coeff, decs) + } } - - // TODO: Reuse dec allocation - newDec, err := LegacyNewDecFromStr(text) - if err != nil { - return err + if x.dec.Negative { + r.Neg(&r) } - - d.i = newDec.i - return nil + return NewIntFromBigInt(&r) } -// MarshalYAML returns the YAML representation. -func (d LegacyDec) MarshalYAML() (interface{}, error) { - return d.String(), nil +func (x Dec) String() string { + return x.dec.Text('f') } -// Marshal implements the gogo proto custom type interface. -func (d LegacyDec) Marshal() ([]byte, error) { - i := d.i - if i == nil { - i = new(big.Int) - } - return i.MarshalText() +// Cmp compares x and y and returns: +// -1 if x < y +// 0 if x == y +// +1 if x > y +// undefined if d or x are NaN +func (x Dec) Cmp(y Dec) int { + return x.dec.Cmp(&y.dec) } -// MarshalTo implements the gogo proto custom type interface. -func (d *LegacyDec) MarshalTo(data []byte) (n int, err error) { - i := d.i - if i == nil { - i = new(big.Int) - } - - if i.Sign() == 0 { - copy(data, []byte{0x30}) - return 1, nil - } - - bz, err := d.Marshal() - if err != nil { - return 0, err - } - - copy(data, bz) - return len(bz), nil +func (x Dec) Equal(y Dec) bool { + return x.dec.Cmp(&y.dec) == 0 } -// Unmarshal implements the gogo proto custom type interface. -func (d *LegacyDec) Unmarshal(data []byte) error { - if len(data) == 0 { - d = nil - return nil - } - - if d.i == nil { - d.i = new(big.Int) - } - - if err := d.i.UnmarshalText(data); err != nil { - return err - } - - if d.i.BitLen() > maxDecBitLen { - return fmt.Errorf("decimal out of range; got: %d, max: %d", d.i.BitLen(), maxDecBitLen) - } - - return nil +// IsZero returns true if the decimal is zero. +func (x Dec) IsZero() bool { + return x.dec.IsZero() } -// Size implements the gogo proto custom type interface. -func (d *LegacyDec) Size() int { - bz, _ := d.Marshal() - return len(bz) +// IsNegative returns true if the decimal is negative. +func (x Dec) IsNegative() bool { + return x.dec.Negative && !x.dec.IsZero() } -// MarshalAmino Override Amino binary serialization by proxying to protobuf. -func (d LegacyDec) MarshalAmino() ([]byte, error) { return d.Marshal() } -func (d *LegacyDec) UnmarshalAmino(bz []byte) error { return d.Unmarshal(bz) } - -// helpers - -// test if two decimal arrays are equal -func LegacyDecsEqual(d1s, d2s []LegacyDec) bool { - if len(d1s) != len(d2s) { - return false - } - - for i, d1 := range d1s { - if !d1.Equal(d2s[i]) { - return false - } - } - return true +// IsPositive returns true if the decimal is positive. +func (x Dec) IsPositive() bool { + return !x.dec.Negative && !x.dec.IsZero() } -// LegacyMinDec minimum decimal between two -func LegacyMinDec(d1, d2 LegacyDec) LegacyDec { - if d1.LT(d2) { - return d1 - } - return d2 +// IsFinite returns true if the decimal is finite. +func (x Dec) IsFinite() bool { + return x.dec.Form == apd.Finite } -// LegacyMaxDec maximum decimal between two -func LegacyMaxDec(d1, d2 LegacyDec) LegacyDec { - if d1.LT(d2) { - return d2 +// NumDecimalPlaces returns the number of decimal places in x. +func (x Dec) NumDecimalPlaces() uint32 { + exp := x.dec.Exponent + if exp >= 0 { + return 0 } - return d1 -} - -// LegacyDecEq intended to be used with require/assert: require.True(DecEq(...)) -func LegacyDecEq(t *testing.T, exp, got LegacyDec) (*testing.T, bool, string, string, string) { - t.Helper() - return t, exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() -} - -func LegacyDecApproxEq(t *testing.T, d1, d2, tol LegacyDec) (*testing.T, bool, string, string, string) { - t.Helper() - diff := d1.Sub(d2).Abs() - return t, diff.LTE(tol), "expected |d1 - d2| <:\t%v\ngot |d1 - d2| = \t\t%v", tol.String(), diff.String() + return uint32(-exp) } -// FormatDec formats a decimal (as encoded in protobuf) into a value-rendered -// string following ADR-050. This function operates with string manipulation -// (instead of manipulating the sdk.Dec object). -func FormatDec(v string) (string, error) { - parts := strings.Split(v, ".") - if len(parts) > 2 { - return "", fmt.Errorf("invalid decimal: too many points in %s", v) - } - - intPart, err := FormatInt(parts[0]) - if err != nil { - return "", err - } - - if len(parts) == 1 { - return intPart, nil - } - - decPart := strings.TrimRight(parts[1], "0") - if len(decPart) == 0 { - return intPart, nil - } - - // Ensure that the decimal part has only digits. - // https://github.com/cosmos/cosmos-sdk/issues/12811 - if !hasOnlyDigits(decPart) { - return "", fmt.Errorf("non-digits detected after decimal point in: %q", decPart) - } - - return intPart + "." + decPart, nil +// Reduce returns a copy of x with all trailing zeros removed and the number +// of trailing zeros removed. +func (x Dec) Reduce() (Dec, int) { + y := Dec{} + _, n := y.dec.Reduce(&x.dec) + return y, n } diff --git a/math/dec_bench_test.go b/math/dec_bench_test.go new file mode 100644 index 000000000000..00ee00a8477d --- /dev/null +++ b/math/dec_bench_test.go @@ -0,0 +1,98 @@ +package math + +import ( + "testing" +) + +func BenchmarkCompareLegacyDecAndNewDec(b *testing.B) { + legacyB1 := LegacyNewDec(100) + legacyB2 := LegacyNewDec(5) + newB1 := NewDecFromInt64(100) + newB2 := NewDecFromInt64(5) + + b.Run("LegacyDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = legacyB1.Quo(legacyB2) + } + }) + + b.Run("NewDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = newB1.Quo(newB2) + } + }) +} + +func BenchmarkCompareLegacyDecAndNewDecQuoInteger(b *testing.B) { + legacyB1 := LegacyNewDec(100) + newB1 := NewDecFromInt64(100) + + b.Run("LegacyDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = legacyB1.Quo(LegacyNewDec(1)) + } + }) + + b.Run("NewDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = newB1.QuoInteger(NewDecFromInt64(1)) + } + }) +} + +func BenchmarkCompareLegacyAddAndDecAdd(b *testing.B) { + legacyB1 := LegacyNewDec(100) + legacyB2 := LegacyNewDec(5) + newB1 := NewDecFromInt64(100) + newB2 := NewDecFromInt64(5) + + b.Run("LegacyDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = legacyB1.Add(legacyB2) + } + }) + + b.Run("NewDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = newB1.Add(newB2) + } + }) +} + +func BenchmarkCompareLegacySubAndDecMul(b *testing.B) { + legacyB1 := LegacyNewDec(100) + legacyB2 := LegacyNewDec(5) + newB1 := NewDecFromInt64(100) + newB2 := NewDecFromInt64(5) + + b.Run("LegacyDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = legacyB1.Mul(legacyB2) + } + }) + + b.Run("NewDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = newB1.Mul(newB2) + } + }) +} + +func BenchmarkCompareLegacySubAndDecSub(b *testing.B) { + legacyB1 := LegacyNewDec(100) + legacyB2 := LegacyNewDec(5) + newB1 := NewDecFromInt64(100) + newB2 := NewDecFromInt64(5) + + b.Run("LegacyDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = legacyB1.Sub(legacyB2) + } + }) + + b.Run("NewDec", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = newB1.Sub(newB2) + } + }) +} \ No newline at end of file diff --git a/math/dec_legacy.go b/math/dec_legacy.go new file mode 100644 index 000000000000..0ca1cfcb8c22 --- /dev/null +++ b/math/dec_legacy.go @@ -0,0 +1,969 @@ +package math + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "strconv" + "strings" + "testing" +) + +// LegacyDec NOTE: never use new(Dec) or else we will panic unmarshalling into the +// nil embedded big.Int +type LegacyDec struct { + i *big.Int +} + +const ( + // LegacyPrecision number of decimal places + LegacyPrecision = 18 + + // LegacyDecimalPrecisionBits bits required to represent the above precision + // Ceiling[Log2[10^Precision - 1]] + LegacyDecimalPrecisionBits = 60 + + // decimalTruncateBits is the minimum number of bits removed + // by a truncate operation. It is equal to + // Floor[Log2[10^Precision - 1]]. + decimalTruncateBits = LegacyDecimalPrecisionBits - 1 + + maxDecBitLen = MaxBitLen + decimalTruncateBits + + // maxApproxRootIterations max number of iterations in ApproxRoot function + maxApproxRootIterations = 300 +) + +var ( + precisionReuse = new(big.Int).Exp(big.NewInt(10), big.NewInt(LegacyPrecision), nil) + fivePrecision = new(big.Int).Quo(precisionReuse, big.NewInt(2)) + precisionMultipliers []*big.Int + zeroInt = big.NewInt(0) + oneInt = big.NewInt(1) + tenInt = big.NewInt(10) + smallestDec = LegacySmallestDec() +) + +// Decimal errors +var ( + ErrLegacyEmptyDecimalStr = errors.New("decimal string cannot be empty") + ErrLegacyInvalidDecimalLength = errors.New("invalid decimal length") + ErrLegacyInvalidDecimalStr = errors.New("invalid decimal string") +) + +// Set precision multipliers +func init() { + precisionMultipliers = make([]*big.Int, LegacyPrecision+1) + for i := 0; i <= LegacyPrecision; i++ { + precisionMultipliers[i] = calcPrecisionMultiplier(int64(i)) + } +} + +func precisionInt() *big.Int { + return new(big.Int).Set(precisionReuse) +} + +func LegacyZeroDec() LegacyDec { return LegacyDec{new(big.Int).Set(zeroInt)} } +func LegacyOneDec() LegacyDec { return LegacyDec{precisionInt()} } +func LegacySmallestDec() LegacyDec { return LegacyDec{new(big.Int).Set(oneInt)} } + +// calculate the precision multiplier +func calcPrecisionMultiplier(prec int64) *big.Int { + if prec < 0 { + panic(fmt.Sprintf("negative precision %v", prec)) + } + + if prec > LegacyPrecision { + panic(fmt.Sprintf("too much precision, maximum %v, provided %v", LegacyPrecision, prec)) + } + zerosToAdd := LegacyPrecision - prec + multiplier := new(big.Int).Exp(tenInt, big.NewInt(zerosToAdd), nil) + return multiplier +} + +// get the precision multiplier, do not mutate result +func precisionMultiplier(prec int64) *big.Int { + if prec < 0 { + panic(fmt.Sprintf("negative precision %v", prec)) + } + + if prec > LegacyPrecision { + panic(fmt.Sprintf("too much precision, maximum %v, provided %v", LegacyPrecision, prec)) + } + return precisionMultipliers[prec] +} + +// LegacyNewDec create a new Dec from integer assuming whole number +func LegacyNewDec(i int64) LegacyDec { + return LegacyNewDecWithPrec(i, 0) +} + +// LegacyNewDecWithPrec create a new Dec from integer with decimal place at prec +// CONTRACT: prec <= Precision +func LegacyNewDecWithPrec(i, prec int64) LegacyDec { + bi := big.NewInt(i) + return LegacyDec{ + bi.Mul(bi, precisionMultiplier(prec)), + } +} + +// LegacyNewDecFromBigInt create a new Dec from big integer assuming whole numbers +// CONTRACT: prec <= Precision +func LegacyNewDecFromBigInt(i *big.Int) LegacyDec { + return LegacyNewDecFromBigIntWithPrec(i, 0) +} + +// LegacyNewDecFromBigIntWithPrec create a new Dec from big integer assuming whole numbers +// CONTRACT: prec <= Precision +func LegacyNewDecFromBigIntWithPrec(i *big.Int, prec int64) LegacyDec { + return LegacyDec{ + new(big.Int).Mul(i, precisionMultiplier(prec)), + } +} + +// LegacyNewDecFromInt create a new Dec from big integer assuming whole numbers +// CONTRACT: prec <= Precision +func LegacyNewDecFromInt(i Int) LegacyDec { + return LegacyNewDecFromIntWithPrec(i, 0) +} + +// LegacyNewDecFromIntWithPrec create a new Dec from big integer with decimal place at prec +// CONTRACT: prec <= Precision +func LegacyNewDecFromIntWithPrec(i Int, prec int64) LegacyDec { + return LegacyDec{ + new(big.Int).Mul(i.BigIntMut(), precisionMultiplier(prec)), + } +} + +// LegacyNewDecFromStr create a decimal from an input decimal string. +// valid must come in the form: +// +// (-) whole integers (.) decimal integers +// +// examples of acceptable input include: +// +// -123.456 +// 456.7890 +// 345 +// -456789 +// +// NOTE - An error will return if more decimal places +// are provided in the string than the constant Precision. +// +// CONTRACT - This function does not mutate the input str. +func LegacyNewDecFromStr(str string) (LegacyDec, error) { + // first extract any negative symbol + neg := false + if len(str) > 0 && str[0] == '-' { + neg = true + str = str[1:] + } + + if len(str) == 0 { + return LegacyDec{}, ErrLegacyEmptyDecimalStr + } + + strs := strings.Split(str, ".") + lenDecs := 0 + combinedStr := strs[0] + + if len(strs) == 2 { // has a decimal place + lenDecs = len(strs[1]) + if lenDecs == 0 || len(combinedStr) == 0 { + return LegacyDec{}, ErrLegacyInvalidDecimalLength + } + combinedStr += strs[1] + } else if len(strs) > 2 { + return LegacyDec{}, ErrLegacyInvalidDecimalStr + } + + if lenDecs > LegacyPrecision { + return LegacyDec{}, fmt.Errorf("value '%s' exceeds max precision by %d decimal places: max precision %d", str, LegacyPrecision-lenDecs, LegacyPrecision) + } + + // add some extra zero's to correct to the Precision factor + zerosToAdd := LegacyPrecision - lenDecs + zeros := strings.Repeat("0", zerosToAdd) + combinedStr += zeros + + combined, ok := new(big.Int).SetString(combinedStr, 10) // base 10 + if !ok { + return LegacyDec{}, fmt.Errorf("failed to set decimal string with base 10: %s", combinedStr) + } + if combined.BitLen() > maxDecBitLen { + return LegacyDec{}, fmt.Errorf("decimal '%s' out of range; bitLen: got %d, max %d", str, combined.BitLen(), maxDecBitLen) + } + if neg { + combined = new(big.Int).Neg(combined) + } + + return LegacyDec{combined}, nil +} + +// LegacyMustNewDecFromStr Decimal from string, panic on error +func LegacyMustNewDecFromStr(s string) LegacyDec { + dec, err := LegacyNewDecFromStr(s) + if err != nil { + panic(err) + } + return dec +} + +func (d LegacyDec) IsNil() bool { return d.i == nil } // is decimal nil +func (d LegacyDec) IsZero() bool { return (d.i).Sign() == 0 } // is equal to zero +func (d LegacyDec) IsNegative() bool { return (d.i).Sign() == -1 } // is negative +func (d LegacyDec) IsPositive() bool { return (d.i).Sign() == 1 } // is positive +func (d LegacyDec) Equal(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) == 0 } // equal decimals +func (d LegacyDec) GT(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) > 0 } // greater than +func (d LegacyDec) GTE(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) >= 0 } // greater than or equal +func (d LegacyDec) LT(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) < 0 } // less than +func (d LegacyDec) LTE(d2 LegacyDec) bool { return (d.i).Cmp(d2.i) <= 0 } // less than or equal +func (d LegacyDec) Neg() LegacyDec { return LegacyDec{new(big.Int).Neg(d.i)} } // reverse the decimal sign +func (d LegacyDec) NegMut() LegacyDec { d.i.Neg(d.i); return d } // reverse the decimal sign, mutable +func (d LegacyDec) Abs() LegacyDec { return LegacyDec{new(big.Int).Abs(d.i)} } // absolute value +func (d LegacyDec) AbsMut() LegacyDec { d.i.Abs(d.i); return d } // absolute value, mutable +func (d LegacyDec) Set(d2 LegacyDec) LegacyDec { d.i.Set(d2.i); return d } // set to existing dec value +func (d LegacyDec) Clone() LegacyDec { return LegacyDec{new(big.Int).Set(d.i)} } // clone new dec + +// BigInt returns a copy of the underlying big.Int. +func (d LegacyDec) BigInt() *big.Int { + if d.IsNil() { + return nil + } + + cp := new(big.Int) + return cp.Set(d.i) +} + +// BigIntMut converts LegacyDec to big.Int, mutative the input +func (d LegacyDec) BigIntMut() *big.Int { + if d.IsNil() { + return nil + } + + return d.i +} + +func (d LegacyDec) ImmutOp(op func(LegacyDec, LegacyDec) LegacyDec, d2 LegacyDec) LegacyDec { + return op(d.Clone(), d2) +} + +func (d LegacyDec) ImmutOpInt(op func(LegacyDec, Int) LegacyDec, d2 Int) LegacyDec { + return op(d.Clone(), d2) +} + +func (d LegacyDec) ImmutOpInt64(op func(LegacyDec, int64) LegacyDec, d2 int64) LegacyDec { + // TODO: use already allocated operand bigint to avoid + // newint each time, add mutex for race condition + // Issue: https://github.com/cosmos/cosmos-sdk/issues/11166 + return op(d.Clone(), d2) +} + +func (d LegacyDec) SetInt64(i int64) LegacyDec { + d.i.SetInt64(i) + d.i.Mul(d.i, precisionReuse) + return d +} + +// Add addition +func (d LegacyDec) Add(d2 LegacyDec) LegacyDec { + return d.ImmutOp(LegacyDec.AddMut, d2) +} + +// AddMut mutable addition +func (d LegacyDec) AddMut(d2 LegacyDec) LegacyDec { + d.i.Add(d.i, d2.i) + + if d.i.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return d +} + +// Sub subtraction +func (d LegacyDec) Sub(d2 LegacyDec) LegacyDec { + return d.ImmutOp(LegacyDec.SubMut, d2) +} + +// SubMut mutable subtraction +func (d LegacyDec) SubMut(d2 LegacyDec) LegacyDec { + d.i.Sub(d.i, d2.i) + + if d.i.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return d +} + +// Mul multiplication +func (d LegacyDec) Mul(d2 LegacyDec) LegacyDec { + return d.ImmutOp(LegacyDec.MulMut, d2) +} + +// MulMut mutable multiplication +func (d LegacyDec) MulMut(d2 LegacyDec) LegacyDec { + d.i.Mul(d.i, d2.i) + chopped := chopPrecisionAndRound(d.i) + + if chopped.BitLen() > maxDecBitLen { + panic("Int overflow") + } + *d.i = *chopped + return d +} + +// MulTruncate multiplication truncate +func (d LegacyDec) MulTruncate(d2 LegacyDec) LegacyDec { + return d.ImmutOp(LegacyDec.MulTruncateMut, d2) +} + +// MulTruncateMut mutable multiplication truncate +func (d LegacyDec) MulTruncateMut(d2 LegacyDec) LegacyDec { + d.i.Mul(d.i, d2.i) + chopPrecisionAndTruncate(d.i) + + if d.i.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return d +} + +// MulRoundUp multiplication round up at precision end. +func (d LegacyDec) MulRoundUp(d2 LegacyDec) LegacyDec { + return d.ImmutOp(LegacyDec.MulRoundUpMut, d2) +} + +// MulRoundUpMut mutable multiplication with round up at precision end. +func (d LegacyDec) MulRoundUpMut(d2 LegacyDec) LegacyDec { + d.i.Mul(d.i, d2.i) + chopPrecisionAndRoundUp(d.i) + + if d.i.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return d +} + +// MulInt multiplication +func (d LegacyDec) MulInt(i Int) LegacyDec { + return d.ImmutOpInt(LegacyDec.MulIntMut, i) +} + +func (d LegacyDec) MulIntMut(i Int) LegacyDec { + d.i.Mul(d.i, i.BigIntMut()) + if d.i.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return d +} + +// MulInt64 multiplication with int64 +func (d LegacyDec) MulInt64(i int64) LegacyDec { + return d.ImmutOpInt64(LegacyDec.MulInt64Mut, i) +} + +func (d LegacyDec) MulInt64Mut(i int64) LegacyDec { + d.i.Mul(d.i, big.NewInt(i)) + + if d.i.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return d +} + +// Quo quotient +func (d LegacyDec) Quo(d2 LegacyDec) LegacyDec { + return d.ImmutOp(LegacyDec.QuoMut, d2) +} + +var squaredPrecisionReuse = new(big.Int).Mul(precisionReuse, precisionReuse) + +// QuoMut mutable quotient +func (d LegacyDec) QuoMut(d2 LegacyDec) LegacyDec { + // multiply by precision twice + d.i.Mul(d.i, squaredPrecisionReuse) + d.i.Quo(d.i, d2.i) + + chopPrecisionAndRound(d.i) + if d.i.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return d +} + +// QuoTruncate quotient truncate +func (d LegacyDec) QuoTruncate(d2 LegacyDec) LegacyDec { + return d.ImmutOp(LegacyDec.QuoTruncateMut, d2) +} + +// QuoTruncateMut mutable quotient truncate +func (d LegacyDec) QuoTruncateMut(d2 LegacyDec) LegacyDec { + // multiply precision twice + d.i.Mul(d.i, squaredPrecisionReuse) + d.i.Quo(d.i, d2.i) + + chopPrecisionAndTruncate(d.i) + if d.i.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return d +} + +// QuoRoundUp quotient, round up +func (d LegacyDec) QuoRoundUp(d2 LegacyDec) LegacyDec { + return d.ImmutOp(LegacyDec.QuoRoundupMut, d2) +} + +// QuoRoundupMut mutable quotient, round up +func (d LegacyDec) QuoRoundupMut(d2 LegacyDec) LegacyDec { + // multiply precision twice + d.i.Mul(d.i, squaredPrecisionReuse) + d.i.Quo(d.i, d2.i) + + chopPrecisionAndRoundUp(d.i) + if d.i.BitLen() > maxDecBitLen { + panic("Int overflow") + } + return d +} + +// QuoInt quotient +func (d LegacyDec) QuoInt(i Int) LegacyDec { + return d.ImmutOpInt(LegacyDec.QuoIntMut, i) +} + +func (d LegacyDec) QuoIntMut(i Int) LegacyDec { + d.i.Quo(d.i, i.BigIntMut()) + return d +} + +// QuoInt64 quotient with int64 +func (d LegacyDec) QuoInt64(i int64) LegacyDec { + return d.ImmutOpInt64(LegacyDec.QuoInt64Mut, i) +} + +func (d LegacyDec) QuoInt64Mut(i int64) LegacyDec { + d.i.Quo(d.i, big.NewInt(i)) + return d +} + +// ApproxRoot returns an approximate estimation of a Dec's positive real nth root +// using Newton's method (where n is positive). The algorithm starts with some guess and +// computes the sequence of improved guesses until an answer converges to an +// approximate answer. It returns `|d|.ApproxRoot() * -1` if input is negative. +// A maximum number of 100 iterations is used a backup boundary condition for +// cases where the answer never converges enough to satisfy the main condition. +func (d LegacyDec) ApproxRoot(root uint64) (guess LegacyDec, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = errors.New("out of bounds") + } + } + }() + + if d.IsNegative() { + absRoot, err := d.Neg().ApproxRoot(root) + return absRoot.NegMut(), err + } + + // One decimal, that we invalidate later. Helps us save a heap allocation. + scratchOneDec := LegacyOneDec() + if root == 1 || d.IsZero() || d.Equal(scratchOneDec) { + return d, nil + } + + if root == 0 { + return scratchOneDec, nil + } + + guess, delta := scratchOneDec, LegacyOneDec() + + for iter := 0; iter < maxApproxRootIterations && delta.Abs().GT(smallestDec); iter++ { + prev := guess.Power(root - 1) + if prev.IsZero() { + prev = smallestDec + } + delta.Set(d).QuoMut(prev) + delta.SubMut(guess) + delta.QuoInt64Mut(int64(root)) + + guess.AddMut(delta) + } + + return guess, nil +} + +// Power returns a the result of raising to a positive integer power +func (d LegacyDec) Power(power uint64) LegacyDec { + res := LegacyDec{new(big.Int).Set(d.i)} + return res.PowerMut(power) +} + +func (d LegacyDec) PowerMut(power uint64) LegacyDec { + if power == 0 { + // Set to 1 with the correct precision. + d.i.Set(precisionReuse) + return d + } + tmp := LegacyOneDec() + + for i := power; i > 1; { + if i%2 != 0 { + tmp.MulMut(d) + } + i /= 2 + d.MulMut(d) + } + + return d.MulMut(tmp) +} + +// ApproxSqrt is a wrapper around ApproxRoot for the common special case +// of finding the square root of a number. It returns -(sqrt(abs(d)) if input is negative. +func (d LegacyDec) ApproxSqrt() (LegacyDec, error) { + return d.ApproxRoot(2) +} + +// IsInteger is integer, e.g. decimals are zero +func (d LegacyDec) IsInteger() bool { + return new(big.Int).Rem(d.i, precisionReuse).Sign() == 0 +} + +// Format format decimal state +func (d LegacyDec) Format(s fmt.State, verb rune) { + _, err := s.Write([]byte(d.String())) + if err != nil { + panic(err) + } +} + +func (d LegacyDec) String() string { + if d.i == nil { + return d.i.String() + } + + isNeg := d.IsNegative() + + if isNeg { + d = d.Neg() + } + + bzInt, err := d.i.MarshalText() + if err != nil { + return "" + } + inputSize := len(bzInt) + + var bzStr []byte + + // TODO: Remove trailing zeros + // case 1, purely decimal + if inputSize <= LegacyPrecision { + bzStr = make([]byte, LegacyPrecision+2) + + // 0. prefix + bzStr[0] = byte('0') + bzStr[1] = byte('.') + + // set relevant digits to 0 + for i := 0; i < LegacyPrecision-inputSize; i++ { + bzStr[i+2] = byte('0') + } + + // set final digits + copy(bzStr[2+(LegacyPrecision-inputSize):], bzInt) + } else { + // inputSize + 1 to account for the decimal point that is being added + bzStr = make([]byte, inputSize+1) + decPointPlace := inputSize - LegacyPrecision + + copy(bzStr, bzInt[:decPointPlace]) // pre-decimal digits + bzStr[decPointPlace] = byte('.') // decimal point + copy(bzStr[decPointPlace+1:], bzInt[decPointPlace:]) // post-decimal digits + } + + if isNeg { + return "-" + string(bzStr) + } + + return string(bzStr) +} + +// Float64 returns the float64 representation of a Dec. +// Will return the error if the conversion failed. +func (d LegacyDec) Float64() (float64, error) { + return strconv.ParseFloat(d.String(), 64) +} + +// MustFloat64 returns the float64 representation of a Dec. +// Would panic if the conversion failed. +func (d LegacyDec) MustFloat64() float64 { + if value, err := strconv.ParseFloat(d.String(), 64); err != nil { + panic(err) + } else { + return value + } +} + +// ____ +// __| |__ "chop 'em +// ` \ round!" +// ___|| ~ _ -bankers +// | | __ +// | | | __|__|__ +// |_____: / | $$$ | +// |________| + +// Remove a Precision amount of rightmost digits and perform bankers rounding +// on the remainder (gaussian rounding) on the digits which have been removed. +// +// Mutates the input. Use the non-mutative version if that is undesired +func chopPrecisionAndRound(d *big.Int) *big.Int { + // remove the negative and add it back when returning + if d.Sign() == -1 { + // make d positive, compute chopped value, and then un-mutate d + d = d.Neg(d) + d = chopPrecisionAndRound(d) + d = d.Neg(d) + return d + } + + // get the truncated quotient and remainder + quo, rem := d, big.NewInt(0) + quo, rem = quo.QuoRem(d, precisionReuse, rem) + + if rem.Sign() == 0 { // remainder is zero + return quo + } + + switch rem.Cmp(fivePrecision) { + case -1: + return quo + case 1: + return quo.Add(quo, oneInt) + default: // bankers rounding must take place + // always round to an even number + if quo.Bit(0) == 0 { + return quo + } + return quo.Add(quo, oneInt) + } +} + +func chopPrecisionAndRoundUp(d *big.Int) *big.Int { + // remove the negative and add it back when returning + if d.Sign() == -1 { + // make d positive, compute chopped value, and then un-mutate d + d = d.Neg(d) + // truncate since d is negative... + chopPrecisionAndTruncate(d) + d = d.Neg(d) + return d + } + + // get the truncated quotient and remainder + quo, rem := d, big.NewInt(0) + quo, rem = quo.QuoRem(d, precisionReuse, rem) + + if rem.Sign() == 0 { // remainder is zero + return quo + } + + return quo.Add(quo, oneInt) +} + +func chopPrecisionAndRoundNonMutative(d *big.Int) *big.Int { + tmp := new(big.Int).Set(d) + return chopPrecisionAndRound(tmp) +} + +// RoundInt64 rounds the decimal using bankers rounding +func (d LegacyDec) RoundInt64() int64 { + chopped := chopPrecisionAndRoundNonMutative(d.i) + if !chopped.IsInt64() { + panic("Int64() out of bound") + } + return chopped.Int64() +} + +// RoundInt round the decimal using bankers rounding +func (d LegacyDec) RoundInt() Int { + return NewIntFromBigIntMut(chopPrecisionAndRoundNonMutative(d.i)) +} + +// chopPrecisionAndTruncate is similar to chopPrecisionAndRound, +// but always rounds down. It does not mutate the input. +func chopPrecisionAndTruncate(d *big.Int) { + d.Quo(d, precisionReuse) +} + +func chopPrecisionAndTruncateNonMutative(d *big.Int) *big.Int { + tmp := new(big.Int).Set(d) + chopPrecisionAndTruncate(tmp) + return tmp +} + +// TruncateInt64 truncates the decimals from the number and returns an int64 +func (d LegacyDec) TruncateInt64() int64 { + chopped := chopPrecisionAndTruncateNonMutative(d.i) + if !chopped.IsInt64() { + panic("Int64() out of bound") + } + return chopped.Int64() +} + +// TruncateInt truncates the decimals from the number and returns an Int +func (d LegacyDec) TruncateInt() Int { + return NewIntFromBigIntMut(chopPrecisionAndTruncateNonMutative(d.i)) +} + +// TruncateDec truncates the decimals from the number and returns a Dec +func (d LegacyDec) TruncateDec() LegacyDec { + return LegacyNewDecFromBigInt(chopPrecisionAndTruncateNonMutative(d.i)) +} + +// Ceil returns the smallest integer value (as a decimal) that is greater than +// or equal to the given decimal. +func (d LegacyDec) Ceil() LegacyDec { + tmp := new(big.Int).Set(d.i) + + quo, rem := tmp, big.NewInt(0) + quo, rem = quo.QuoRem(tmp, precisionReuse, rem) + + // no need to round with a zero remainder regardless of sign + if rem.Sign() == 0 { + return LegacyNewDecFromBigInt(quo) + } else if rem.Sign() == -1 { + return LegacyNewDecFromBigInt(quo) + } + + if d.i.BitLen() >= maxDecBitLen { + panic("Int overflow") + } + + return LegacyNewDecFromBigInt(quo.Add(quo, oneInt)) +} + +// LegacyMaxSortableDec is the largest Dec that can be passed into SortableDecBytes() +// Its negative form is the least Dec that can be passed in. +var LegacyMaxSortableDec LegacyDec + +func init() { + LegacyMaxSortableDec = LegacyOneDec().Quo(LegacySmallestDec()) +} + +// LegacyValidSortableDec ensures that a Dec is within the sortable bounds, +// a Dec can't have a precision of less than 10^-18. +// Max sortable decimal was set to the reciprocal of SmallestDec. +func LegacyValidSortableDec(dec LegacyDec) bool { + return dec.Abs().LTE(LegacyMaxSortableDec) +} + +// LegacySortableDecBytes returns a byte slice representation of a Dec that can be sorted. +// Left and right pads with 0s so there are 18 digits to left and right of the decimal point. +// For this reason, there is a maximum and minimum value for this, enforced by ValidSortableDec. +func LegacySortableDecBytes(dec LegacyDec) []byte { + if !LegacyValidSortableDec(dec) { + panic("dec must be within bounds") + } + // Instead of adding an extra byte to all sortable decs in order to handle max sortable, we just + // makes its bytes be "max" which comes after all numbers in ASCIIbetical order + if dec.Equal(LegacyMaxSortableDec) { + return []byte("max") + } + // For the same reason, we make the bytes of minimum sortable dec be --, which comes before all numbers. + if dec.Equal(LegacyMaxSortableDec.Neg()) { + return []byte("--") + } + // We move the negative sign to the front of all the left padded 0s, to make negative numbers come before positive numbers + if dec.IsNegative() { + return append([]byte("-"), []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", LegacyPrecision*2+1), dec.Abs().String()))...) + } + return []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", LegacyPrecision*2+1), dec.String())) +} + +// reuse nil values +var nilJSON []byte + +func init() { + empty := new(big.Int) + bz, _ := empty.MarshalText() + nilJSON, _ = json.Marshal(string(bz)) +} + +// MarshalJSON marshals the decimal +func (d LegacyDec) MarshalJSON() ([]byte, error) { + if d.i == nil { + return nilJSON, nil + } + return json.Marshal(d.String()) +} + +// UnmarshalJSON defines custom decoding scheme +func (d *LegacyDec) UnmarshalJSON(bz []byte) error { + if d.i == nil { + d.i = new(big.Int) + } + + var text string + err := json.Unmarshal(bz, &text) + if err != nil { + return err + } + + // TODO: Reuse dec allocation + newDec, err := LegacyNewDecFromStr(text) + if err != nil { + return err + } + + d.i = newDec.i + return nil +} + +// MarshalYAML returns the YAML representation. +func (d LegacyDec) MarshalYAML() (interface{}, error) { + return d.String(), nil +} + +// Marshal implements the gogo proto custom type interface. +func (d LegacyDec) Marshal() ([]byte, error) { + i := d.i + if i == nil { + i = new(big.Int) + } + return i.MarshalText() +} + +// MarshalTo implements the gogo proto custom type interface. +func (d *LegacyDec) MarshalTo(data []byte) (n int, err error) { + i := d.i + if i == nil { + i = new(big.Int) + } + + if i.Sign() == 0 { + copy(data, []byte{0x30}) + return 1, nil + } + + bz, err := d.Marshal() + if err != nil { + return 0, err + } + + copy(data, bz) + return len(bz), nil +} + +// Unmarshal implements the gogo proto custom type interface. +func (d *LegacyDec) Unmarshal(data []byte) error { + if len(data) == 0 { + d = nil + return nil + } + + if d.i == nil { + d.i = new(big.Int) + } + + if err := d.i.UnmarshalText(data); err != nil { + return err + } + + if d.i.BitLen() > maxDecBitLen { + return fmt.Errorf("decimal out of range; got: %d, max: %d", d.i.BitLen(), maxDecBitLen) + } + + return nil +} + +// Size implements the gogo proto custom type interface. +func (d *LegacyDec) Size() int { + bz, _ := d.Marshal() + return len(bz) +} + +// MarshalAmino Override Amino binary serialization by proxying to protobuf. +func (d LegacyDec) MarshalAmino() ([]byte, error) { return d.Marshal() } +func (d *LegacyDec) UnmarshalAmino(bz []byte) error { return d.Unmarshal(bz) } + +// helpers + +// test if two decimal arrays are equal +func LegacyDecsEqual(d1s, d2s []LegacyDec) bool { + if len(d1s) != len(d2s) { + return false + } + + for i, d1 := range d1s { + if !d1.Equal(d2s[i]) { + return false + } + } + return true +} + +// LegacyMinDec minimum decimal between two +func LegacyMinDec(d1, d2 LegacyDec) LegacyDec { + if d1.LT(d2) { + return d1 + } + return d2 +} + +// LegacyMaxDec maximum decimal between two +func LegacyMaxDec(d1, d2 LegacyDec) LegacyDec { + if d1.LT(d2) { + return d2 + } + return d1 +} + +// LegacyDecEq intended to be used with require/assert: require.True(DecEq(...)) +func LegacyDecEq(t *testing.T, exp, got LegacyDec) (*testing.T, bool, string, string, string) { + t.Helper() + return t, exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() +} + +func LegacyDecApproxEq(t *testing.T, d1, d2, tol LegacyDec) (*testing.T, bool, string, string, string) { + t.Helper() + diff := d1.Sub(d2).Abs() + return t, diff.LTE(tol), "expected |d1 - d2| <:\t%v\ngot |d1 - d2| = \t\t%v", tol.String(), diff.String() +} + +// FormatDec formats a decimal (as encoded in protobuf) into a value-rendered +// string following ADR-050. This function operates with string manipulation +// (instead of manipulating the sdk.Dec object). +func FormatDec(v string) (string, error) { + parts := strings.Split(v, ".") + if len(parts) > 2 { + return "", fmt.Errorf("invalid decimal: too many points in %s", v) + } + + intPart, err := FormatInt(parts[0]) + if err != nil { + return "", err + } + + if len(parts) == 1 { + return intPart, nil + } + + decPart := strings.TrimRight(parts[1], "0") + if len(decPart) == 0 { + return intPart, nil + } + + // Ensure that the decimal part has only digits. + // https://github.com/cosmos/cosmos-sdk/issues/12811 + if !hasOnlyDigits(decPart) { + return "", fmt.Errorf("non-digits detected after decimal point in: %q", decPart) + } + + return intPart + "." + decPart, nil +} diff --git a/math/dec_legacy_test.go b/math/dec_legacy_test.go new file mode 100644 index 000000000000..96d7231a9413 --- /dev/null +++ b/math/dec_legacy_test.go @@ -0,0 +1,784 @@ +package math_test + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "sigs.k8s.io/yaml" + + "cosmossdk.io/math" +) + +type decimalTestSuite struct { + suite.Suite +} + +func TestDecimalTestSuite(t *testing.T) { + suite.Run(t, new(decimalTestSuite)) +} + +func TestDecApproxEq(t *testing.T) { + // d1 = 0.55, d2 = 0.6, tol = 0.1 + d1 := math.LegacyNewDecWithPrec(55, 2) + d2 := math.LegacyNewDecWithPrec(6, 1) + tol := math.LegacyNewDecWithPrec(1, 1) + + require.True(math.LegacyDecApproxEq(t, d1, d2, tol)) + + // d1 = 0.55, d2 = 0.6, tol = 1E-5 + d1 = math.LegacyNewDecWithPrec(55, 2) + d2 = math.LegacyNewDecWithPrec(6, 1) + tol = math.LegacyNewDecWithPrec(1, 5) + + require.False(math.LegacyDecApproxEq(t, d1, d2, tol)) + + // d1 = 0.6, d2 = 0.61, tol = 0.01 + d1 = math.LegacyNewDecWithPrec(6, 1) + d2 = math.LegacyNewDecWithPrec(61, 2) + tol = math.LegacyNewDecWithPrec(1, 2) + + require.True(math.LegacyDecApproxEq(t, d1, d2, tol)) +} + +// create a decimal from a decimal string (ex. "1234.5678") +func (s *decimalTestSuite) mustNewDecFromStr(str string) (d math.LegacyDec) { + d, err := math.LegacyNewDecFromStr(str) + s.Require().NoError(err) + + return d +} + +func (s *decimalTestSuite) TestNewDecFromStr() { + largeBigInt, ok := new(big.Int).SetString("3144605511029693144278234343371835", 10) + s.Require().True(ok) + + largerBigInt, ok := new(big.Int).SetString("8888888888888888888888888888888888888888888888888888888888888888888844444440", 10) + s.Require().True(ok) + + largestBigInt, ok := new(big.Int).SetString("33499189745056880149688856635597007162669032647290798121690100488888732861290034376435130433535", 10) + s.Require().True(ok) + + tests := []struct { + decimalStr string + expErr bool + exp math.LegacyDec + }{ + {"", true, math.LegacyDec{}}, + {"0.-75", true, math.LegacyDec{}}, + {"0", false, math.LegacyNewDec(0)}, + {"1", false, math.LegacyNewDec(1)}, + {"1.1", false, math.LegacyNewDecWithPrec(11, 1)}, + {"0.75", false, math.LegacyNewDecWithPrec(75, 2)}, + {"0.8", false, math.LegacyNewDecWithPrec(8, 1)}, + {"0.11111", false, math.LegacyNewDecWithPrec(11111, 5)}, + {"314460551102969.3144278234343371835", true, math.LegacyNewDec(3141203149163817869)}, + { + "314460551102969314427823434337.1835718092488231350", + true, math.LegacyNewDecFromBigIntWithPrec(largeBigInt, 4), + }, + { + "314460551102969314427823434337.1835", + false, math.LegacyNewDecFromBigIntWithPrec(largeBigInt, 4), + }, + {".", true, math.LegacyDec{}}, + {".0", true, math.LegacyNewDec(0)}, + {"1.", true, math.LegacyNewDec(1)}, + {"foobar", true, math.LegacyDec{}}, + {"0.foobar", true, math.LegacyDec{}}, + {"0.foobar.", true, math.LegacyDec{}}, + {"8888888888888888888888888888888888888888888888888888888888888888888844444440", false, math.LegacyNewDecFromBigInt(largerBigInt)}, + {"33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535", false, math.LegacyNewDecFromBigIntWithPrec(largestBigInt, 18)}, + {"133499189745056880149688856635597007162669032647290798121690100488888732861291", true, math.LegacyDec{}}, + } + + for tcIndex, tc := range tests { + res, err := math.LegacyNewDecFromStr(tc.decimalStr) + if tc.expErr { + s.Require().NotNil(err, "error expected, decimalStr %v, tc %v", tc.decimalStr, tcIndex) + } else { + s.Require().Nil(err, "unexpected error, decimalStr %v, tc %v", tc.decimalStr, tcIndex) + s.Require().True(res.Equal(tc.exp), "equality was incorrect, res %v, exp %v, tc %v", res, tc.exp, tcIndex) + } + + // negative tc + res, err = math.LegacyNewDecFromStr("-" + tc.decimalStr) + if tc.expErr { + s.Require().NotNil(err, "error expected, decimalStr %v, tc %v", tc.decimalStr, tcIndex) + } else { + s.Require().Nil(err, "unexpected error, decimalStr %v, tc %v", tc.decimalStr, tcIndex) + exp := tc.exp.Mul(math.LegacyNewDec(-1)) + s.Require().True(res.Equal(exp), "equality was incorrect, res %v, exp %v, tc %v", res, exp, tcIndex) + } + } +} + +func (s *decimalTestSuite) TestDecString() { + tests := []struct { + d math.LegacyDec + want string + }{ + {math.LegacyNewDec(0), "0.000000000000000000"}, + {math.LegacyNewDec(1), "1.000000000000000000"}, + {math.LegacyNewDec(10), "10.000000000000000000"}, + {math.LegacyNewDec(12340), "12340.000000000000000000"}, + {math.LegacyNewDecWithPrec(12340, 4), "1.234000000000000000"}, + {math.LegacyNewDecWithPrec(12340, 5), "0.123400000000000000"}, + {math.LegacyNewDecWithPrec(12340, 8), "0.000123400000000000"}, + {math.LegacyNewDecWithPrec(1009009009009009009, 17), "10.090090090090090090"}, + } + for tcIndex, tc := range tests { + s.Require().Equal(tc.want, tc.d.String(), "bad String(), index: %v", tcIndex) + } +} + +func (s *decimalTestSuite) TestDecFloat64() { + tests := []struct { + d math.LegacyDec + want float64 + }{ + {math.LegacyNewDec(0), 0.000000000000000000}, + {math.LegacyNewDec(1), 1.000000000000000000}, + {math.LegacyNewDec(10), 10.000000000000000000}, + {math.LegacyNewDec(12340), 12340.000000000000000000}, + {math.LegacyNewDecWithPrec(12340, 4), 1.234000000000000000}, + {math.LegacyNewDecWithPrec(12340, 5), 0.123400000000000000}, + {math.LegacyNewDecWithPrec(12340, 8), 0.000123400000000000}, + {math.LegacyNewDecWithPrec(1009009009009009009, 17), 10.090090090090090090}, + } + for tcIndex, tc := range tests { + value, err := tc.d.Float64() + s.Require().Nil(err, "error getting Float64(), index: %v", tcIndex) + s.Require().Equal(tc.want, value, "bad Float64(), index: %v", tcIndex) + s.Require().Equal(tc.want, tc.d.MustFloat64(), "bad MustFloat64(), index: %v", tcIndex) + } +} + +func (s *decimalTestSuite) TestEqualities() { + tests := []struct { + d1, d2 math.LegacyDec + gt, lt, eq bool + }{ + {math.LegacyNewDec(0), math.LegacyNewDec(0), false, false, true}, + {math.LegacyNewDecWithPrec(0, 2), math.LegacyNewDecWithPrec(0, 4), false, false, true}, + {math.LegacyNewDecWithPrec(100, 0), math.LegacyNewDecWithPrec(100, 0), false, false, true}, + {math.LegacyNewDecWithPrec(-100, 0), math.LegacyNewDecWithPrec(-100, 0), false, false, true}, + {math.LegacyNewDecWithPrec(-1, 1), math.LegacyNewDecWithPrec(-1, 1), false, false, true}, + {math.LegacyNewDecWithPrec(3333, 3), math.LegacyNewDecWithPrec(3333, 3), false, false, true}, + + {math.LegacyNewDecWithPrec(0, 0), math.LegacyNewDecWithPrec(3333, 3), false, true, false}, + {math.LegacyNewDecWithPrec(0, 0), math.LegacyNewDecWithPrec(100, 0), false, true, false}, + {math.LegacyNewDecWithPrec(-1, 0), math.LegacyNewDecWithPrec(3333, 3), false, true, false}, + {math.LegacyNewDecWithPrec(-1, 0), math.LegacyNewDecWithPrec(100, 0), false, true, false}, + {math.LegacyNewDecWithPrec(1111, 3), math.LegacyNewDecWithPrec(100, 0), false, true, false}, + {math.LegacyNewDecWithPrec(1111, 3), math.LegacyNewDecWithPrec(3333, 3), false, true, false}, + {math.LegacyNewDecWithPrec(-3333, 3), math.LegacyNewDecWithPrec(-1111, 3), false, true, false}, + + {math.LegacyNewDecWithPrec(3333, 3), math.LegacyNewDecWithPrec(0, 0), true, false, false}, + {math.LegacyNewDecWithPrec(100, 0), math.LegacyNewDecWithPrec(0, 0), true, false, false}, + {math.LegacyNewDecWithPrec(3333, 3), math.LegacyNewDecWithPrec(-1, 0), true, false, false}, + {math.LegacyNewDecWithPrec(100, 0), math.LegacyNewDecWithPrec(-1, 0), true, false, false}, + {math.LegacyNewDecWithPrec(100, 0), math.LegacyNewDecWithPrec(1111, 3), true, false, false}, + {math.LegacyNewDecWithPrec(3333, 3), math.LegacyNewDecWithPrec(1111, 3), true, false, false}, + {math.LegacyNewDecWithPrec(-1111, 3), math.LegacyNewDecWithPrec(-3333, 3), true, false, false}, + } + + for tcIndex, tc := range tests { + s.Require().Equal(tc.gt, tc.d1.GT(tc.d2), "GT result is incorrect, tc %d", tcIndex) + s.Require().Equal(tc.lt, tc.d1.LT(tc.d2), "LT result is incorrect, tc %d", tcIndex) + s.Require().Equal(tc.eq, tc.d1.Equal(tc.d2), "equality result is incorrect, tc %d", tcIndex) + } +} + +func (s *decimalTestSuite) TestDecsEqual() { + tests := []struct { + d1s, d2s []math.LegacyDec + eq bool + }{ + {[]math.LegacyDec{math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(0)}, true}, + {[]math.LegacyDec{math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(1)}, false}, + {[]math.LegacyDec{math.LegacyNewDec(0)}, []math.LegacyDec{}, false}, + {[]math.LegacyDec{math.LegacyNewDec(0), math.LegacyNewDec(1)}, []math.LegacyDec{math.LegacyNewDec(0), math.LegacyNewDec(1)}, true}, + {[]math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(0)}, true}, + {[]math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(0), math.LegacyNewDec(1)}, false}, + {[]math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(1)}, false}, + {[]math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(2)}, []math.LegacyDec{math.LegacyNewDec(2), math.LegacyNewDec(4)}, false}, + {[]math.LegacyDec{math.LegacyNewDec(3), math.LegacyNewDec(18)}, []math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(6)}, false}, + } + + for tcIndex, tc := range tests { + s.Require().Equal(tc.eq, math.LegacyDecsEqual(tc.d1s, tc.d2s), "equality of decional arrays is incorrect, tc %d", tcIndex) + s.Require().Equal(tc.eq, math.LegacyDecsEqual(tc.d2s, tc.d1s), "equality of decional arrays is incorrect (converse), tc %d", tcIndex) + } +} + +func (s *decimalTestSuite) TestArithmetic() { + tests := []struct { + d1, d2 math.LegacyDec + expMul, expMulTruncate, expMulRoundUp math.LegacyDec + expQuo, expQuoRoundUp, expQuoTruncate math.LegacyDec + expAdd, expSub math.LegacyDec + }{ + // d1 d2 MUL MulTruncate MulRoundUp QUO QUORoundUp QUOTrunctate ADD SUB + {math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0)}, + {math.LegacyNewDec(1), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(1), math.LegacyNewDec(1)}, + {math.LegacyNewDec(0), math.LegacyNewDec(1), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(1), math.LegacyNewDec(-1)}, + {math.LegacyNewDec(0), math.LegacyNewDec(-1), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(-1), math.LegacyNewDec(1)}, + {math.LegacyNewDec(-1), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(-1), math.LegacyNewDec(-1)}, + + {math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(2), math.LegacyNewDec(0)}, + {math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(-2), math.LegacyNewDec(0)}, + {math.LegacyNewDec(1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(0), math.LegacyNewDec(2)}, + {math.LegacyNewDec(-1), math.LegacyNewDec(1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(0), math.LegacyNewDec(-2)}, + + { + math.LegacyNewDec(3), math.LegacyNewDec(7), math.LegacyNewDec(21), math.LegacyNewDec(21), math.LegacyNewDec(21), + math.LegacyNewDecWithPrec(428571428571428571, 18), math.LegacyNewDecWithPrec(428571428571428572, 18), math.LegacyNewDecWithPrec(428571428571428571, 18), + math.LegacyNewDec(10), math.LegacyNewDec(-4), + }, + { + math.LegacyNewDec(2), math.LegacyNewDec(4), math.LegacyNewDec(8), math.LegacyNewDec(8), math.LegacyNewDec(8), math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDecWithPrec(5, 1), + math.LegacyNewDec(6), math.LegacyNewDec(-2), + }, + + {math.LegacyNewDec(100), math.LegacyNewDec(100), math.LegacyNewDec(10000), math.LegacyNewDec(10000), math.LegacyNewDec(10000), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(200), math.LegacyNewDec(0)}, + + { + math.LegacyNewDecWithPrec(15, 1), math.LegacyNewDecWithPrec(15, 1), math.LegacyNewDecWithPrec(225, 2), math.LegacyNewDecWithPrec(225, 2), math.LegacyNewDecWithPrec(225, 2), + math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(3), math.LegacyNewDec(0), + }, + { + math.LegacyNewDecWithPrec(3333, 4), math.LegacyNewDecWithPrec(333, 4), math.LegacyNewDecWithPrec(1109889, 8), math.LegacyNewDecWithPrec(1109889, 8), math.LegacyNewDecWithPrec(1109889, 8), + math.LegacyMustNewDecFromStr("10.009009009009009009"), math.LegacyMustNewDecFromStr("10.009009009009009010"), math.LegacyMustNewDecFromStr("10.009009009009009009"), + math.LegacyNewDecWithPrec(3666, 4), math.LegacyNewDecWithPrec(3, 1), + }, + } + + for tcIndex, tc := range tests { + tc := tc + resAdd := tc.d1.Add(tc.d2) + resSub := tc.d1.Sub(tc.d2) + resMul := tc.d1.Mul(tc.d2) + resMulTruncate := tc.d1.MulTruncate(tc.d2) + resMulRoundUp := tc.d1.MulRoundUp(tc.d2) + s.Require().True(tc.expAdd.Equal(resAdd), "exp %v, res %v, tc %d", tc.expAdd, resAdd, tcIndex) + s.Require().True(tc.expSub.Equal(resSub), "exp %v, res %v, tc %d", tc.expSub, resSub, tcIndex) + s.Require().True(tc.expMul.Equal(resMul), "exp %v, res %v, tc %d", tc.expMul, resMul, tcIndex) + s.Require().True(tc.expMulTruncate.Equal(resMulTruncate), "exp %v, res %v, tc %d", tc.expMulTruncate, resMulTruncate, tcIndex) + s.Require().True(tc.expMulRoundUp.Equal(resMulRoundUp), "exp %v, res %v, tc %d", tc.expMulRoundUp, resMulRoundUp, tcIndex) + + if tc.d2.IsZero() { // panic for divide by zero + s.Require().Panics(func() { tc.d1.Quo(tc.d2) }) + } else { + resQuo := tc.d1.Quo(tc.d2) + s.Require().True(tc.expQuo.Equal(resQuo), "exp %v, res %v, tc %d", tc.expQuo.String(), resQuo.String(), tcIndex) + + resQuoRoundUp := tc.d1.QuoRoundUp(tc.d2) + s.Require().True(tc.expQuoRoundUp.Equal(resQuoRoundUp), "exp %v, res %v, tc %d", + tc.expQuoRoundUp.String(), resQuoRoundUp.String(), tcIndex) + + resQuoTruncate := tc.d1.QuoTruncate(tc.d2) + s.Require().True(tc.expQuoTruncate.Equal(resQuoTruncate), "exp %v, res %v, tc %d", + tc.expQuoTruncate.String(), resQuoTruncate.String(), tcIndex) + } + } +} + +func (s *decimalTestSuite) TestMulRoundUp_RoundingAtPrecisionEnd() { + var ( + a = math.LegacyMustNewDecFromStr("0.000000000000000009") + b = math.LegacyMustNewDecFromStr("0.000000000000000009") + expectedRoundUp = math.LegacyMustNewDecFromStr("0.000000000000000001") + expectedTruncate = math.LegacyMustNewDecFromStr("0.000000000000000000") + ) + + actualRoundUp := a.MulRoundUp(b) + s.Require().Equal(expectedRoundUp.String(), actualRoundUp.String(), "exp %v, res %v", expectedRoundUp, actualRoundUp) + + actualTruncate := a.MulTruncate(b) + s.Require().Equal(expectedTruncate.String(), actualTruncate.String(), "exp %v, res %v", expectedRoundUp, actualTruncate) +} + +func (s *decimalTestSuite) TestBankerRoundChop() { + tests := []struct { + d1 math.LegacyDec + exp int64 + }{ + {s.mustNewDecFromStr("0.25"), 0}, + {s.mustNewDecFromStr("0"), 0}, + {s.mustNewDecFromStr("1"), 1}, + {s.mustNewDecFromStr("0.75"), 1}, + {s.mustNewDecFromStr("0.5"), 0}, + {s.mustNewDecFromStr("7.5"), 8}, + {s.mustNewDecFromStr("1.5"), 2}, + {s.mustNewDecFromStr("2.5"), 2}, + {s.mustNewDecFromStr("0.545"), 1}, // 0.545-> 1 even though 5 is first decimal and 1 not even + {s.mustNewDecFromStr("1.545"), 2}, + } + + for tcIndex, tc := range tests { + resNeg := tc.d1.Neg().RoundInt64() + s.Require().Equal(-1*tc.exp, resNeg, "negative tc %d", tcIndex) + + resPos := tc.d1.RoundInt64() + s.Require().Equal(tc.exp, resPos, "positive tc %d", tcIndex) + } +} + +func (s *decimalTestSuite) TestTruncate() { + tests := []struct { + d1 math.LegacyDec + exp int64 + }{ + {s.mustNewDecFromStr("0"), 0}, + {s.mustNewDecFromStr("0.25"), 0}, + {s.mustNewDecFromStr("0.75"), 0}, + {s.mustNewDecFromStr("1"), 1}, + {s.mustNewDecFromStr("1.5"), 1}, + {s.mustNewDecFromStr("7.5"), 7}, + {s.mustNewDecFromStr("7.6"), 7}, + {s.mustNewDecFromStr("7.4"), 7}, + {s.mustNewDecFromStr("100.1"), 100}, + {s.mustNewDecFromStr("1000.1"), 1000}, + } + + for tcIndex, tc := range tests { + resNeg := tc.d1.Neg().TruncateInt64() + s.Require().Equal(-1*tc.exp, resNeg, "negative tc %d", tcIndex) + + resPos := tc.d1.TruncateInt64() + s.Require().Equal(tc.exp, resPos, "positive tc %d", tcIndex) + } +} + +func (s *decimalTestSuite) TestStringOverflow() { + // two random 64 bit primes + dec1, err := math.LegacyNewDecFromStr("51643150036226787134389711697696177267") + s.Require().NoError(err) + dec2, err := math.LegacyNewDecFromStr("-31798496660535729618459429845579852627") + s.Require().NoError(err) + dec3 := dec1.Add(dec2) + s.Require().Equal( + "19844653375691057515930281852116324640.000000000000000000", + dec3.String(), + ) +} + +func (s *decimalTestSuite) TestDecMulInt() { + tests := []struct { + sdkDec math.LegacyDec + sdkInt math.Int + want math.LegacyDec + }{ + {math.LegacyNewDec(10), math.NewInt(2), math.LegacyNewDec(20)}, + {math.LegacyNewDec(1000000), math.NewInt(100), math.LegacyNewDec(100000000)}, + {math.LegacyNewDecWithPrec(1, 1), math.NewInt(10), math.LegacyNewDec(1)}, + {math.LegacyNewDecWithPrec(1, 5), math.NewInt(20), math.LegacyNewDecWithPrec(2, 4)}, + } + for i, tc := range tests { + got := tc.sdkDec.MulInt(tc.sdkInt) + s.Require().Equal(tc.want, got, "Incorrect result on test case %d", i) + } +} + +func (s *decimalTestSuite) TestDecCeil() { + testCases := []struct { + input math.LegacyDec + expected math.LegacyDec + }{ + {math.LegacyNewDecWithPrec(1000000000000000, math.LegacyPrecision), math.LegacyNewDec(1)}, // 0.001 => 1.0 + {math.LegacyNewDecWithPrec(-1000000000000000, math.LegacyPrecision), math.LegacyZeroDec()}, // -0.001 => 0.0 + {math.LegacyZeroDec(), math.LegacyZeroDec()}, // 0.0 => 0.0 + {math.LegacyNewDecWithPrec(900000000000000000, math.LegacyPrecision), math.LegacyNewDec(1)}, // 0.9 => 1.0 + {math.LegacyNewDecWithPrec(4001000000000000000, math.LegacyPrecision), math.LegacyNewDec(5)}, // 4.001 => 5.0 + {math.LegacyNewDecWithPrec(-4001000000000000000, math.LegacyPrecision), math.LegacyNewDec(-4)}, // -4.001 => -4.0 + {math.LegacyNewDecWithPrec(4700000000000000000, math.LegacyPrecision), math.LegacyNewDec(5)}, // 4.7 => 5.0 + {math.LegacyNewDecWithPrec(-4700000000000000000, math.LegacyPrecision), math.LegacyNewDec(-4)}, // -4.7 => -4.0 + } + + for i, tc := range testCases { + res := tc.input.Ceil() + s.Require().Equal(tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) + } +} + +func (s *decimalTestSuite) TestCeilOverflow() { + d, err := math.LegacyNewDecFromStr("66749594872528440074844428317798503581334516323645399060845050244444366430645.000000000000000001") + s.Require().NoError(err) + s.Require().True(d.BigInt().BitLen() <= 315, "d is too large") + // this call panics because the value is too large + s.Require().Panics(func() { d.Ceil() }, "Ceil should panic on overflow") +} + +func (s *decimalTestSuite) TestPower() { + testCases := []struct { + input math.LegacyDec + power uint64 + expected math.LegacyDec + }{ + {math.LegacyNewDec(100), 0, math.LegacyOneDec()}, // 10 ^ (0) => 1.0 + {math.LegacyOneDec(), 10, math.LegacyOneDec()}, // 1.0 ^ (10) => 1.0 + {math.LegacyNewDecWithPrec(5, 1), 2, math.LegacyNewDecWithPrec(25, 2)}, // 0.5 ^ 2 => 0.25 + {math.LegacyNewDecWithPrec(2, 1), 2, math.LegacyNewDecWithPrec(4, 2)}, // 0.2 ^ 2 => 0.04 + {math.LegacyNewDecFromInt(math.NewInt(3)), 3, math.LegacyNewDecFromInt(math.NewInt(27))}, // 3 ^ 3 => 27 + {math.LegacyNewDecFromInt(math.NewInt(-3)), 4, math.LegacyNewDecFromInt(math.NewInt(81))}, // -3 ^ 4 = 81 + {math.LegacyNewDecWithPrec(1414213562373095049, 18), 2, math.LegacyNewDecFromInt(math.NewInt(2))}, // 1.414213562373095049 ^ 2 = 2 + } + + for i, tc := range testCases { + res := tc.input.Power(tc.power) + s.Require().True(tc.expected.Sub(res).Abs().LTE(math.LegacySmallestDec()), "unexpected result for test case %d, normal power, input: %v", i, tc.input) + + mutableInput := tc.input + mutableInput.PowerMut(tc.power) + s.Require().True(tc.expected.Sub(mutableInput).Abs().LTE(math.LegacySmallestDec()), + "unexpected result for test case %d, input %v", i, tc.input) + s.Require().True(res.Equal(tc.input), "unexpected result for test case %d, mutable power, input: %v", i, tc.input) + } +} + +func (s *decimalTestSuite) TestApproxRoot() { + testCases := []struct { + input math.LegacyDec + root uint64 + expected math.LegacyDec + }{ + {math.LegacyOneDec(), 10, math.LegacyOneDec()}, // 1.0 ^ (0.1) => 1.0 + {math.LegacyNewDecWithPrec(25, 2), 2, math.LegacyNewDecWithPrec(5, 1)}, // 0.25 ^ (0.5) => 0.5 + {math.LegacyNewDecWithPrec(4, 2), 2, math.LegacyNewDecWithPrec(2, 1)}, // 0.04 ^ (0.5) => 0.2 + {math.LegacyNewDecFromInt(math.NewInt(27)), 3, math.LegacyNewDecFromInt(math.NewInt(3))}, // 27 ^ (1/3) => 3 + {math.LegacyNewDecFromInt(math.NewInt(-81)), 4, math.LegacyNewDecFromInt(math.NewInt(-3))}, // -81 ^ (0.25) => -3 + {math.LegacyNewDecFromInt(math.NewInt(2)), 2, math.LegacyNewDecWithPrec(1414213562373095049, 18)}, // 2 ^ (0.5) => 1.414213562373095049 + {math.LegacyNewDecWithPrec(1005, 3), 31536000, math.LegacyMustNewDecFromStr("1.000000000158153904")}, // 1.005 ^ (1/31536000) ≈ 1.00000000016 + {math.LegacySmallestDec(), 2, math.LegacyNewDecWithPrec(1, 9)}, // 1e-18 ^ (0.5) => 1e-9 + {math.LegacySmallestDec(), 3, math.LegacyMustNewDecFromStr("0.000000999999999997")}, // 1e-18 ^ (1/3) => 1e-6 + {math.LegacyNewDecWithPrec(1, 8), 3, math.LegacyMustNewDecFromStr("0.002154434690031900")}, // 1e-8 ^ (1/3) ≈ 0.00215443469 + {math.LegacyMustNewDecFromStr("9000002314687921634000000000000000000021394871242000000000000000"), 2, math.LegacyMustNewDecFromStr("94868342004527103646332858502867.899477053226766107")}, + } + + // In the case of 1e-8 ^ (1/3), the result repeats every 5 iterations starting from iteration 24 + // (i.e. 24, 29, 34, ... give the same result) and never converges enough. The maximum number of + // iterations (300) causes the result at iteration 300 to be returned, regardless of convergence. + + for i, tc := range testCases { + res, err := tc.input.ApproxRoot(tc.root) + s.Require().NoError(err) + s.Require().True(tc.expected.Sub(res).Abs().LTE(math.LegacySmallestDec()), "unexpected result for test case %d, input: %v", i, tc.input) + } +} + +func (s *decimalTestSuite) TestApproxSqrt() { + testCases := []struct { + input math.LegacyDec + expected math.LegacyDec + }{ + {math.LegacyOneDec(), math.LegacyOneDec()}, // 1.0 => 1.0 + {math.LegacyNewDecWithPrec(25, 2), math.LegacyNewDecWithPrec(5, 1)}, // 0.25 => 0.5 + {math.LegacyNewDecWithPrec(4, 2), math.LegacyNewDecWithPrec(2, 1)}, // 0.09 => 0.3 + {math.LegacyNewDec(9), math.LegacyNewDecFromInt(math.NewInt(3))}, // 9 => 3 + {math.LegacyNewDec(-9), math.LegacyNewDecFromInt(math.NewInt(-3))}, // -9 => -3 + {math.LegacyNewDec(2), math.LegacyNewDecWithPrec(1414213562373095049, 18)}, // 2 => 1.414213562373095049 + { // 2^127 - 1 => 13043817825332782212.3495718062525083688 which rounds to 13043817825332782212.3495718062525083689 + math.LegacyNewDec(2).Power(127).Sub(math.LegacyOneDec()), + math.LegacyMustNewDecFromStr("13043817825332782212.349571806252508369"), + }, + {math.LegacyMustNewDecFromStr("1.000000011823380862"), math.LegacyMustNewDecFromStr("1.000000005911690414")}, + } + + for i, tc := range testCases { + res, err := tc.input.ApproxSqrt() + s.Require().NoError(err) + s.Require().Equal(tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) + } +} + +func (s *decimalTestSuite) TestDecSortableBytes() { + tests := []struct { + d math.LegacyDec + want []byte + }{ + {math.LegacyNewDec(0), []byte("000000000000000000.000000000000000000")}, + {math.LegacyNewDec(1), []byte("000000000000000001.000000000000000000")}, + {math.LegacyNewDec(10), []byte("000000000000000010.000000000000000000")}, + {math.LegacyNewDec(12340), []byte("000000000000012340.000000000000000000")}, + {math.LegacyNewDecWithPrec(12340, 4), []byte("000000000000000001.234000000000000000")}, + {math.LegacyNewDecWithPrec(12340, 5), []byte("000000000000000000.123400000000000000")}, + {math.LegacyNewDecWithPrec(12340, 8), []byte("000000000000000000.000123400000000000")}, + {math.LegacyNewDecWithPrec(1009009009009009009, 17), []byte("000000000000000010.090090090090090090")}, + {math.LegacyNewDecWithPrec(-1009009009009009009, 17), []byte("-000000000000000010.090090090090090090")}, + {math.LegacyNewDec(1000000000000000000), []byte("max")}, + {math.LegacyNewDec(-1000000000000000000), []byte("--")}, + } + for tcIndex, tc := range tests { + s.Require().Equal(tc.want, math.LegacySortableDecBytes(tc.d), "bad String(), index: %v", tcIndex) + } + + s.Require().Panics(func() { math.LegacySortableDecBytes(math.LegacyNewDec(1000000000000000001)) }) + s.Require().Panics(func() { math.LegacySortableDecBytes(math.LegacyNewDec(-1000000000000000001)) }) +} + +func (s *decimalTestSuite) TestDecEncoding() { + largestBigInt, ok := new(big.Int).SetString("33499189745056880149688856635597007162669032647290798121690100488888732861290034376435130433535", 10) + s.Require().True(ok) + + smallestBigInt, ok := new(big.Int).SetString("-33499189745056880149688856635597007162669032647290798121690100488888732861290034376435130433535", 10) + s.Require().True(ok) + + const maxDecBitLen = 315 + maxInt, ok := new(big.Int).SetString(strings.Repeat("1", maxDecBitLen), 2) + s.Require().True(ok) + + testCases := []struct { + input math.LegacyDec + rawBz string + jsonStr string + yamlStr string + }{ + { + math.LegacyNewDec(0), "30", + "\"0.000000000000000000\"", + "\"0.000000000000000000\"\n", + }, + { + math.LegacyNewDecWithPrec(4, 2), + "3430303030303030303030303030303030", + "\"0.040000000000000000\"", + "\"0.040000000000000000\"\n", + }, + { + math.LegacyNewDecWithPrec(-4, 2), + "2D3430303030303030303030303030303030", + "\"-0.040000000000000000\"", + "\"-0.040000000000000000\"\n", + }, + { + math.LegacyNewDecWithPrec(1414213562373095049, 18), + "31343134323133353632333733303935303439", + "\"1.414213562373095049\"", + "\"1.414213562373095049\"\n", + }, + { + math.LegacyNewDecWithPrec(-1414213562373095049, 18), + "2D31343134323133353632333733303935303439", + "\"-1.414213562373095049\"", + "\"-1.414213562373095049\"\n", + }, + { + math.LegacyNewDecFromBigIntWithPrec(largestBigInt, 18), + "3333343939313839373435303536383830313439363838383536363335353937303037313632363639303332363437323930373938313231363930313030343838383838373332383631323930303334333736343335313330343333353335", + "\"33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535\"", + "\"33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535\"\n", + }, + { + math.LegacyNewDecFromBigIntWithPrec(smallestBigInt, 18), + "2D3333343939313839373435303536383830313439363838383536363335353937303037313632363639303332363437323930373938313231363930313030343838383838373332383631323930303334333736343335313330343333353335", + "\"-33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535\"", + "\"-33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535\"\n", + }, + { + math.LegacyNewDecFromBigIntWithPrec(maxInt, 18), + "3636373439353934383732353238343430303734383434343238333137373938353033353831333334353136333233363435333939303630383435303530323434343434333636343330363435303137313838323137353635323136373637", + "\"66749594872528440074844428317798503581334516323645399060845050244444366430645.017188217565216767\"", + "\"66749594872528440074844428317798503581334516323645399060845050244444366430645.017188217565216767\"\n", + }, + } + + for _, tc := range testCases { + bz, err := tc.input.Marshal() + s.Require().NoError(err) + s.Require().Equal(tc.rawBz, fmt.Sprintf("%X", bz)) + + var other math.LegacyDec + s.Require().NoError((&other).Unmarshal(bz)) + s.Require().True(tc.input.Equal(other)) + + bz, err = json.Marshal(tc.input) + s.Require().NoError(err) + s.Require().Equal(tc.jsonStr, string(bz)) + s.Require().NoError(json.Unmarshal(bz, &other)) + s.Require().True(tc.input.Equal(other)) + + bz, err = yaml.Marshal(tc.input) + s.Require().NoError(err) + s.Require().Equal(tc.yamlStr, string(bz)) + } +} + +// Showcase that different orders of operations causes different results. +func (s *decimalTestSuite) TestOperationOrders() { + n1 := math.LegacyNewDec(10) + n2 := math.LegacyNewDec(1000000010) + s.Require().Equal(n1.Mul(n2).Quo(n2), math.LegacyNewDec(10)) + s.Require().NotEqual(n1.Mul(n2).Quo(n2), n1.Quo(n2).Mul(n2)) +} + +func BenchmarkMarshalTo(b *testing.B) { + b.ReportAllocs() + bis := []struct { + in math.LegacyDec + want []byte + }{ + { + math.LegacyNewDec(1e8), []byte{ + 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + }, + }, + {math.LegacyNewDec(0), []byte{0x30}}, + } + data := make([]byte, 100) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + for _, bi := range bis { + if n, err := bi.in.MarshalTo(data); err != nil { + b.Fatal(err) + } else if !bytes.Equal(data[:n], bi.want) { + b.Fatalf("Mismatch\nGot: % x\nWant: % x\n", data[:n], bi.want) + } + } + } +} + +var sink interface{} + +func BenchmarkLegacyQuoMut(b *testing.B) { + b1 := math.LegacyNewDec(17e2 + 8371) + b2 := math.LegacyNewDec(4371) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sink = b1.QuoMut(b2) + } + + if sink == nil { + b.Fatal("Benchmark did not run") + } + sink = (interface{})(nil) +} + +func BenchmarkLegacyQuoTruncateMut(b *testing.B) { + b1 := math.LegacyNewDec(17e2 + 8371) + b2 := math.LegacyNewDec(4371) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sink = b1.QuoTruncateMut(b2) + } + + if sink == nil { + b.Fatal("Benchmark did not run") + } + sink = (interface{})(nil) +} + +func BenchmarkLegacySqrtOnMersennePrime(b *testing.B) { + b1 := math.LegacyNewDec(2).Power(127).Sub(math.LegacyOneDec()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sink, _ = b1.ApproxSqrt() + } + + if sink == nil { + b.Fatal("Benchmark did not run") + } + sink = (interface{})(nil) +} + +func BenchmarkLegacyQuoRoundupMut(b *testing.B) { + b1 := math.LegacyNewDec(17e2 + 8371) + b2 := math.LegacyNewDec(4371) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sink = b1.QuoRoundupMut(b2) + } + + if sink == nil { + b.Fatal("Benchmark did not run") + } + sink = (interface{})(nil) +} + +func TestFormatDec(t *testing.T) { + type decimalTest []string + var testcases []decimalTest + raw, err := os.ReadFile("./testdata/decimals.json") + require.NoError(t, err) + err = json.Unmarshal(raw, &testcases) + require.NoError(t, err) + + for _, tc := range testcases { + tc := tc + t.Run(tc[0], func(t *testing.T) { + out, err := math.FormatDec(tc[0]) + require.NoError(t, err) + require.Equal(t, tc[1], out) + }) + } +} + +func TestFormatDecNonDigits(t *testing.T) { + badCases := []string{ + "10.a", + "1a.10", + "p1a10.", + "0.10p", + "--10", + "12.😎😎", + "11111111111133333333333333333333333333333a", + "11111111111133333333333333333333333333333 192892", + } + + for _, value := range badCases { + value := value + t.Run(value, func(t *testing.T) { + s, err := math.FormatDec(value) + if err == nil { + t.Fatal("Expected an error") + } + if g, w := err.Error(), "non-digits"; !strings.Contains(g, w) { + t.Errorf("Error mismatch\nGot: %q\nWant substring: %q", g, w) + } + if s != "" { + t.Fatalf("Got a non-empty string: %q", s) + } + }) + } +} + +func TestNegativePrecisionPanic(t *testing.T) { + require.Panics(t, func() { + math.LegacyNewDecWithPrec(10, -1) + }) +} + +func (s *decimalTestSuite) TestConvertToBigIntMutativeForLegacyDec() { + r := big.NewInt(30) + i := math.LegacyNewDecFromBigInt(r) + + // Compare value of BigInt & BigIntMut + s.Require().Equal(i.BigInt(), i.BigIntMut()) + + // Modify BigIntMut() pointer and ensure i.BigIntMut() & i.BigInt() change + p1 := i.BigIntMut() + p1.SetInt64(40) + s.Require().Equal(big.NewInt(40), i.BigIntMut()) + s.Require().Equal(big.NewInt(40), i.BigInt()) + + // Modify big.Int() pointer and ensure i.BigIntMut() & i.BigInt() don't change + p2 := i.BigInt() + p2.SetInt64(50) + s.Require().NotEqual(big.NewInt(50), i.BigIntMut()) + s.Require().NotEqual(big.NewInt(50), i.BigInt()) +} diff --git a/math/dec_test.go b/math/dec_test.go index 96d7231a9413..b921435a09c7 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -1,784 +1,713 @@ -package math_test +package math import ( - "bytes" - "encoding/json" "fmt" - "math/big" - "os" + "regexp" + "strconv" "strings" "testing" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - "sigs.k8s.io/yaml" - - "cosmossdk.io/math" + "pgregory.net/rapid" ) -type decimalTestSuite struct { - suite.Suite +func TestDec(t *testing.T) { + + // Property tests + t.Run("TestNewDecFromInt64", rapid.MakeCheck(testDecInt64)) + + // Properties about *FromString functions + t.Run("TestInvalidNewDecFromString", rapid.MakeCheck(testInvalidNewDecFromString)) + t.Run("TestInvalidNewNonNegativeDecFromString", rapid.MakeCheck(testInvalidNewNonNegativeDecFromString)) + t.Run("TestInvalidNewNonNegativeFixedDecFromString", rapid.MakeCheck(testInvalidNewNonNegativeFixedDecFromString)) + t.Run("TestInvalidNewPositiveDecFromString", rapid.MakeCheck(testInvalidNewPositiveDecFromString)) + t.Run("TestInvalidNewPositiveFixedDecFromString", rapid.MakeCheck(testInvalidNewPositiveFixedDecFromString)) + + // Properties about addition + t.Run("TestAddLeftIdentity", rapid.MakeCheck(testAddLeftIdentity)) + t.Run("TestAddRightIdentity", rapid.MakeCheck(testAddRightIdentity)) + t.Run("TestAddCommutative", rapid.MakeCheck(testAddCommutative)) + t.Run("TestAddAssociative", rapid.MakeCheck(testAddAssociative)) + + // Properties about subtraction + t.Run("TestSubRightIdentity", rapid.MakeCheck(testSubRightIdentity)) + t.Run("TestSubZero", rapid.MakeCheck(testSubZero)) + + // Properties about multiplication + t.Run("TestMulLeftIdentity", rapid.MakeCheck(testMulLeftIdentity)) + t.Run("TestMulRightIdentity", rapid.MakeCheck(testMulRightIdentity)) + t.Run("TestMulCommutative", rapid.MakeCheck(testMulCommutative)) + t.Run("TestMulAssociative", rapid.MakeCheck(testMulAssociative)) + t.Run("TestZeroIdentity", rapid.MakeCheck(testMulZero)) + + // Properties about division + t.Run("TestDivisionBySelf", rapid.MakeCheck(testSelfQuo)) + t.Run("TestDivisionByOne", rapid.MakeCheck(testQuoByOne)) + + // Properties combining operations + t.Run("TestSubAdd", rapid.MakeCheck(testSubAdd)) + t.Run("TestAddSub", rapid.MakeCheck(testAddSub)) + t.Run("TestMulQuoA", rapid.MakeCheck(testMulQuoA)) + t.Run("TestMulQuoB", rapid.MakeCheck(testMulQuoB)) + t.Run("TestMulQuoExact", rapid.MakeCheck(testMulQuoExact)) + t.Run("TestQuoMulExact", rapid.MakeCheck(testQuoMulExact)) + + // Properties about comparison and equality + t.Run("TestCmpInverse", rapid.MakeCheck(testCmpInverse)) + t.Run("TestEqualCommutative", rapid.MakeCheck(testEqualCommutative)) + + // Properties about tests on a single Dec + t.Run("TestIsZero", rapid.MakeCheck(testIsZero)) + t.Run("TestIsNegative", rapid.MakeCheck(testIsNegative)) + t.Run("TestIsPositive", rapid.MakeCheck(testIsPositive)) + t.Run("TestNumDecimalPlaces", rapid.MakeCheck(testNumDecimalPlaces)) + + // Unit tests + zero := Dec{} + one := NewDecFromInt64(1) + two := NewDecFromInt64(2) + three := NewDecFromInt64(3) + four := NewDecFromInt64(4) + five := NewDecFromInt64(5) + minusOne := NewDecFromInt64(-1) + + onePointOneFive, err := NewDecFromString("1.15") + require.NoError(t, err) + twoPointThreeFour, err := NewDecFromString("2.34") + require.NoError(t, err) + threePointFourNine, err := NewDecFromString("3.49") + require.NoError(t, err) + onePointFourNine, err := NewDecFromString("1.49") + require.NoError(t, err) + minusFivePointZero, err := NewDecFromString("-5.0") + require.NoError(t, err) + + twoThousand := NewDecFinite(2, 3) + require.True(t, twoThousand.Equal(NewDecFromInt64(2000))) + + res, err := two.Add(zero) + require.NoError(t, err) + require.True(t, res.Equal(two)) + + res, err = five.Sub(two) + require.NoError(t, err) + require.True(t, res.Equal(three)) + + res, err = SafeSubBalance(five, two) + require.NoError(t, err) + require.True(t, res.Equal(three)) + + _, err = SafeSubBalance(two, five) + require.Error(t, err, "Expected insufficient funds error") + + res, err = SafeAddBalance(three, two) + require.NoError(t, err) + require.True(t, res.Equal(five)) + + _, err = SafeAddBalance(minusFivePointZero, five) + require.Error(t, err, "Expected ErrInvalidRequest") + + res, err = four.Quo(two) + require.NoError(t, err) + require.True(t, res.Equal(two)) + + res, err = five.QuoInteger(two) + require.NoError(t, err) + require.True(t, res.Equal(two)) + + res, err = five.Rem(two) + require.NoError(t, err) + require.True(t, res.Equal(one)) + + x, err := four.Int64() + require.NoError(t, err) + require.Equal(t, int64(4), x) + + require.Equal(t, "5", five.String()) + + res, err = onePointOneFive.Add(twoPointThreeFour) + require.NoError(t, err) + require.True(t, res.Equal(threePointFourNine)) + + res, err = threePointFourNine.Sub(two) + require.NoError(t, err) + require.True(t, res.Equal(onePointFourNine)) + + res, err = minusOne.Sub(four) + require.NoError(t, err) + require.True(t, res.Equal(minusFivePointZero)) + + require.True(t, zero.IsZero()) + require.False(t, zero.IsPositive()) + require.False(t, zero.IsNegative()) + + require.False(t, one.IsZero()) + require.True(t, one.IsPositive()) + require.False(t, one.IsNegative()) + + require.False(t, minusOne.IsZero()) + require.False(t, minusOne.IsPositive()) + require.True(t, minusOne.IsNegative()) + + res, err = one.MulExact(two) + require.NoError(t, err) + require.True(t, res.Equal(two)) } -func TestDecimalTestSuite(t *testing.T) { - suite.Run(t, new(decimalTestSuite)) +// TODO: Think a bit more about the probability distribution of Dec +var genDec *rapid.Generator[Dec] = rapid.Custom(func(t *rapid.T) Dec { + f := rapid.Float64().Draw(t, "f") + dec, err := NewDecFromString(fmt.Sprintf("%g", f)) + require.NoError(t, err) + return dec +}) + +// A Dec value and the float used to create it +type floatAndDec struct { + float float64 + dec Dec } -func TestDecApproxEq(t *testing.T) { - // d1 = 0.55, d2 = 0.6, tol = 0.1 - d1 := math.LegacyNewDecWithPrec(55, 2) - d2 := math.LegacyNewDecWithPrec(6, 1) - tol := math.LegacyNewDecWithPrec(1, 1) +// Generate a Dec value along with the float used to create it +var genFloatAndDec *rapid.Generator[floatAndDec] = rapid.Custom(func(t *rapid.T) floatAndDec { + f := rapid.Float64().Draw(t, "f") + dec, err := NewDecFromString(fmt.Sprintf("%g", f)) + require.NoError(t, err) + return floatAndDec{f, dec} +}) - require.True(math.LegacyDecApproxEq(t, d1, d2, tol)) +// Property: n == NewDecFromInt64(n).Int64() +func testDecInt64(t *rapid.T) { + nIn := rapid.Int64().Draw(t, "n") + nOut, err := NewDecFromInt64(nIn).Int64() - // d1 = 0.55, d2 = 0.6, tol = 1E-5 - d1 = math.LegacyNewDecWithPrec(55, 2) - d2 = math.LegacyNewDecWithPrec(6, 1) - tol = math.LegacyNewDecWithPrec(1, 5) + require.NoError(t, err) + require.Equal(t, nIn, nOut) +} + +// Property: invalid_number_string(s) => NewDecFromString(s) == err +func testInvalidNewDecFromString(t *rapid.T) { + s := rapid.StringMatching("[[:alpha:]]+").Draw(t, "s") + _, err := NewDecFromString(s) + require.Error(t, err) +} + +// Property: invalid_number_string(s) || IsNegative(s) +// => NewNonNegativeDecFromString(s) == err +func testInvalidNewNonNegativeDecFromString(t *rapid.T) { + s := rapid.OneOf( + rapid.StringMatching("[[:alpha:]]+"), + rapid.StringMatching(`^-\d*\.?\d+$`).Filter( + func(s string) bool { return !strings.HasPrefix(s, "-0") && !strings.HasPrefix(s, "-.0") }, + ), + ).Draw(t, "s") + _, err := NewNonNegativeDecFromString(s) + require.Error(t, err) +} + +// Property: invalid_number_string(s) || IsNegative(s) || NumDecimals(s) > n +// => NewNonNegativeFixedDecFromString(s, n) == err +func testInvalidNewNonNegativeFixedDecFromString(t *rapid.T) { + n := rapid.Uint32Range(0, 999).Draw(t, "n") + s := rapid.OneOf( + rapid.StringMatching("[[:alpha:]]+"), + rapid.StringMatching(`^-\d*\.?\d+$`).Filter( + func(s string) bool { return !strings.HasPrefix(s, "-0") && !strings.HasPrefix(s, "-.0") }, + ), + rapid.StringMatching(fmt.Sprintf(`\d*\.\d{%d,}`, n+1)), + ).Draw(t, "s") + _, err := NewNonNegativeFixedDecFromString(s, n) + require.Error(t, err) +} + +// Property: invalid_number_string(s) || IsNegative(s) || IsZero(s) +// => NewPositiveDecFromString(s) == err +func testInvalidNewPositiveDecFromString(t *rapid.T) { + s := rapid.OneOf( + rapid.StringMatching("[[:alpha:]]+"), + rapid.StringMatching(`^-\d*\.?\d+|0$`), + ).Draw(t, "s") + _, err := NewPositiveDecFromString(s) + require.Error(t, err) +} + +// Property: invalid_number_string(s) || IsNegative(s) || IsZero(s) || NumDecimals(s) > n +// => NewPositiveFixedDecFromString(s) == err +func testInvalidNewPositiveFixedDecFromString(t *rapid.T) { + n := rapid.Uint32Range(0, 999).Draw(t, "n") + s := rapid.OneOf( + rapid.StringMatching("[[:alpha:]]+"), + rapid.StringMatching(`^-\d*\.?\d+|0$`), + rapid.StringMatching(fmt.Sprintf(`\d*\.\d{%d,}`, n+1)), + ).Draw(t, "s") + _, err := NewPositiveFixedDecFromString(s, n) + require.Error(t, err) +} + +// Property: 0 + a == a +func testAddLeftIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := NewDecFromInt64(0) + + b, err := zero.Add(a) + require.NoError(t, err) - require.False(math.LegacyDecApproxEq(t, d1, d2, tol)) + require.True(t, a.Equal(b)) +} + +// Property: a + 0 == a +func testAddRightIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := NewDecFromInt64(0) - // d1 = 0.6, d2 = 0.61, tol = 0.01 - d1 = math.LegacyNewDecWithPrec(6, 1) - d2 = math.LegacyNewDecWithPrec(61, 2) - tol = math.LegacyNewDecWithPrec(1, 2) + b, err := a.Add(zero) + require.NoError(t, err) - require.True(math.LegacyDecApproxEq(t, d1, d2, tol)) + require.True(t, a.Equal(b)) } -// create a decimal from a decimal string (ex. "1234.5678") -func (s *decimalTestSuite) mustNewDecFromStr(str string) (d math.LegacyDec) { - d, err := math.LegacyNewDecFromStr(str) - s.Require().NoError(err) +// Property: a + b == b + a +func testAddCommutative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + + c, err := a.Add(b) + require.NoError(t, err) + + d, err := b.Add(a) + require.NoError(t, err) - return d + require.True(t, c.Equal(d)) } -func (s *decimalTestSuite) TestNewDecFromStr() { - largeBigInt, ok := new(big.Int).SetString("3144605511029693144278234343371835", 10) - s.Require().True(ok) +// Property: (a + b) + c == a + (b + c) +func testAddAssociative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + c := genDec.Draw(t, "c") - largerBigInt, ok := new(big.Int).SetString("8888888888888888888888888888888888888888888888888888888888888888888844444440", 10) - s.Require().True(ok) + // (a + b) + c + d, err := a.Add(b) + require.NoError(t, err) - largestBigInt, ok := new(big.Int).SetString("33499189745056880149688856635597007162669032647290798121690100488888732861290034376435130433535", 10) - s.Require().True(ok) + e, err := d.Add(c) + require.NoError(t, err) - tests := []struct { - decimalStr string - expErr bool - exp math.LegacyDec - }{ - {"", true, math.LegacyDec{}}, - {"0.-75", true, math.LegacyDec{}}, - {"0", false, math.LegacyNewDec(0)}, - {"1", false, math.LegacyNewDec(1)}, - {"1.1", false, math.LegacyNewDecWithPrec(11, 1)}, - {"0.75", false, math.LegacyNewDecWithPrec(75, 2)}, - {"0.8", false, math.LegacyNewDecWithPrec(8, 1)}, - {"0.11111", false, math.LegacyNewDecWithPrec(11111, 5)}, - {"314460551102969.3144278234343371835", true, math.LegacyNewDec(3141203149163817869)}, - { - "314460551102969314427823434337.1835718092488231350", - true, math.LegacyNewDecFromBigIntWithPrec(largeBigInt, 4), - }, - { - "314460551102969314427823434337.1835", - false, math.LegacyNewDecFromBigIntWithPrec(largeBigInt, 4), - }, - {".", true, math.LegacyDec{}}, - {".0", true, math.LegacyNewDec(0)}, - {"1.", true, math.LegacyNewDec(1)}, - {"foobar", true, math.LegacyDec{}}, - {"0.foobar", true, math.LegacyDec{}}, - {"0.foobar.", true, math.LegacyDec{}}, - {"8888888888888888888888888888888888888888888888888888888888888888888844444440", false, math.LegacyNewDecFromBigInt(largerBigInt)}, - {"33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535", false, math.LegacyNewDecFromBigIntWithPrec(largestBigInt, 18)}, - {"133499189745056880149688856635597007162669032647290798121690100488888732861291", true, math.LegacyDec{}}, - } + // a + (b + c) + f, err := b.Add(c) + require.NoError(t, err) - for tcIndex, tc := range tests { - res, err := math.LegacyNewDecFromStr(tc.decimalStr) - if tc.expErr { - s.Require().NotNil(err, "error expected, decimalStr %v, tc %v", tc.decimalStr, tcIndex) - } else { - s.Require().Nil(err, "unexpected error, decimalStr %v, tc %v", tc.decimalStr, tcIndex) - s.Require().True(res.Equal(tc.exp), "equality was incorrect, res %v, exp %v, tc %v", res, tc.exp, tcIndex) - } + g, err := a.Add(f) + require.NoError(t, err) - // negative tc - res, err = math.LegacyNewDecFromStr("-" + tc.decimalStr) - if tc.expErr { - s.Require().NotNil(err, "error expected, decimalStr %v, tc %v", tc.decimalStr, tcIndex) - } else { - s.Require().Nil(err, "unexpected error, decimalStr %v, tc %v", tc.decimalStr, tcIndex) - exp := tc.exp.Mul(math.LegacyNewDec(-1)) - s.Require().True(res.Equal(exp), "equality was incorrect, res %v, exp %v, tc %v", res, exp, tcIndex) - } - } + require.True(t, e.Equal(g)) } -func (s *decimalTestSuite) TestDecString() { - tests := []struct { - d math.LegacyDec - want string - }{ - {math.LegacyNewDec(0), "0.000000000000000000"}, - {math.LegacyNewDec(1), "1.000000000000000000"}, - {math.LegacyNewDec(10), "10.000000000000000000"}, - {math.LegacyNewDec(12340), "12340.000000000000000000"}, - {math.LegacyNewDecWithPrec(12340, 4), "1.234000000000000000"}, - {math.LegacyNewDecWithPrec(12340, 5), "0.123400000000000000"}, - {math.LegacyNewDecWithPrec(12340, 8), "0.000123400000000000"}, - {math.LegacyNewDecWithPrec(1009009009009009009, 17), "10.090090090090090090"}, - } - for tcIndex, tc := range tests { - s.Require().Equal(tc.want, tc.d.String(), "bad String(), index: %v", tcIndex) - } +// Property: a - 0 == a +func testSubRightIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := NewDecFromInt64(0) + + b, err := a.Sub(zero) + require.NoError(t, err) + + require.True(t, a.Equal(b)) } -func (s *decimalTestSuite) TestDecFloat64() { - tests := []struct { - d math.LegacyDec - want float64 - }{ - {math.LegacyNewDec(0), 0.000000000000000000}, - {math.LegacyNewDec(1), 1.000000000000000000}, - {math.LegacyNewDec(10), 10.000000000000000000}, - {math.LegacyNewDec(12340), 12340.000000000000000000}, - {math.LegacyNewDecWithPrec(12340, 4), 1.234000000000000000}, - {math.LegacyNewDecWithPrec(12340, 5), 0.123400000000000000}, - {math.LegacyNewDecWithPrec(12340, 8), 0.000123400000000000}, - {math.LegacyNewDecWithPrec(1009009009009009009, 17), 10.090090090090090090}, - } - for tcIndex, tc := range tests { - value, err := tc.d.Float64() - s.Require().Nil(err, "error getting Float64(), index: %v", tcIndex) - s.Require().Equal(tc.want, value, "bad Float64(), index: %v", tcIndex) - s.Require().Equal(tc.want, tc.d.MustFloat64(), "bad MustFloat64(), index: %v", tcIndex) - } +// Property: a - a == 0 +func testSubZero(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := NewDecFromInt64(0) + + b, err := a.Sub(a) + require.NoError(t, err) + + require.True(t, b.Equal(zero)) } -func (s *decimalTestSuite) TestEqualities() { - tests := []struct { - d1, d2 math.LegacyDec - gt, lt, eq bool - }{ - {math.LegacyNewDec(0), math.LegacyNewDec(0), false, false, true}, - {math.LegacyNewDecWithPrec(0, 2), math.LegacyNewDecWithPrec(0, 4), false, false, true}, - {math.LegacyNewDecWithPrec(100, 0), math.LegacyNewDecWithPrec(100, 0), false, false, true}, - {math.LegacyNewDecWithPrec(-100, 0), math.LegacyNewDecWithPrec(-100, 0), false, false, true}, - {math.LegacyNewDecWithPrec(-1, 1), math.LegacyNewDecWithPrec(-1, 1), false, false, true}, - {math.LegacyNewDecWithPrec(3333, 3), math.LegacyNewDecWithPrec(3333, 3), false, false, true}, - - {math.LegacyNewDecWithPrec(0, 0), math.LegacyNewDecWithPrec(3333, 3), false, true, false}, - {math.LegacyNewDecWithPrec(0, 0), math.LegacyNewDecWithPrec(100, 0), false, true, false}, - {math.LegacyNewDecWithPrec(-1, 0), math.LegacyNewDecWithPrec(3333, 3), false, true, false}, - {math.LegacyNewDecWithPrec(-1, 0), math.LegacyNewDecWithPrec(100, 0), false, true, false}, - {math.LegacyNewDecWithPrec(1111, 3), math.LegacyNewDecWithPrec(100, 0), false, true, false}, - {math.LegacyNewDecWithPrec(1111, 3), math.LegacyNewDecWithPrec(3333, 3), false, true, false}, - {math.LegacyNewDecWithPrec(-3333, 3), math.LegacyNewDecWithPrec(-1111, 3), false, true, false}, - - {math.LegacyNewDecWithPrec(3333, 3), math.LegacyNewDecWithPrec(0, 0), true, false, false}, - {math.LegacyNewDecWithPrec(100, 0), math.LegacyNewDecWithPrec(0, 0), true, false, false}, - {math.LegacyNewDecWithPrec(3333, 3), math.LegacyNewDecWithPrec(-1, 0), true, false, false}, - {math.LegacyNewDecWithPrec(100, 0), math.LegacyNewDecWithPrec(-1, 0), true, false, false}, - {math.LegacyNewDecWithPrec(100, 0), math.LegacyNewDecWithPrec(1111, 3), true, false, false}, - {math.LegacyNewDecWithPrec(3333, 3), math.LegacyNewDecWithPrec(1111, 3), true, false, false}, - {math.LegacyNewDecWithPrec(-1111, 3), math.LegacyNewDecWithPrec(-3333, 3), true, false, false}, - } +// Property: 1 * a == a +func testMulLeftIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + one := NewDecFromInt64(1) - for tcIndex, tc := range tests { - s.Require().Equal(tc.gt, tc.d1.GT(tc.d2), "GT result is incorrect, tc %d", tcIndex) - s.Require().Equal(tc.lt, tc.d1.LT(tc.d2), "LT result is incorrect, tc %d", tcIndex) - s.Require().Equal(tc.eq, tc.d1.Equal(tc.d2), "equality result is incorrect, tc %d", tcIndex) - } + b, err := one.Mul(a) + require.NoError(t, err) + + require.True(t, a.Equal(b)) } -func (s *decimalTestSuite) TestDecsEqual() { - tests := []struct { - d1s, d2s []math.LegacyDec - eq bool - }{ - {[]math.LegacyDec{math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(0)}, true}, - {[]math.LegacyDec{math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(1)}, false}, - {[]math.LegacyDec{math.LegacyNewDec(0)}, []math.LegacyDec{}, false}, - {[]math.LegacyDec{math.LegacyNewDec(0), math.LegacyNewDec(1)}, []math.LegacyDec{math.LegacyNewDec(0), math.LegacyNewDec(1)}, true}, - {[]math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(0)}, true}, - {[]math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(0), math.LegacyNewDec(1)}, false}, - {[]math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(0)}, []math.LegacyDec{math.LegacyNewDec(1)}, false}, - {[]math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(2)}, []math.LegacyDec{math.LegacyNewDec(2), math.LegacyNewDec(4)}, false}, - {[]math.LegacyDec{math.LegacyNewDec(3), math.LegacyNewDec(18)}, []math.LegacyDec{math.LegacyNewDec(1), math.LegacyNewDec(6)}, false}, - } +// Property: a * 1 == a +func testMulRightIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + one := NewDecFromInt64(1) - for tcIndex, tc := range tests { - s.Require().Equal(tc.eq, math.LegacyDecsEqual(tc.d1s, tc.d2s), "equality of decional arrays is incorrect, tc %d", tcIndex) - s.Require().Equal(tc.eq, math.LegacyDecsEqual(tc.d2s, tc.d1s), "equality of decional arrays is incorrect (converse), tc %d", tcIndex) - } + b, err := a.Mul(one) + require.NoError(t, err) + + require.True(t, a.Equal(b)) } -func (s *decimalTestSuite) TestArithmetic() { - tests := []struct { - d1, d2 math.LegacyDec - expMul, expMulTruncate, expMulRoundUp math.LegacyDec - expQuo, expQuoRoundUp, expQuoTruncate math.LegacyDec - expAdd, expSub math.LegacyDec - }{ - // d1 d2 MUL MulTruncate MulRoundUp QUO QUORoundUp QUOTrunctate ADD SUB - {math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0)}, - {math.LegacyNewDec(1), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(1), math.LegacyNewDec(1)}, - {math.LegacyNewDec(0), math.LegacyNewDec(1), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(1), math.LegacyNewDec(-1)}, - {math.LegacyNewDec(0), math.LegacyNewDec(-1), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(-1), math.LegacyNewDec(1)}, - {math.LegacyNewDec(-1), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(0), math.LegacyNewDec(-1), math.LegacyNewDec(-1)}, - - {math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(2), math.LegacyNewDec(0)}, - {math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(-2), math.LegacyNewDec(0)}, - {math.LegacyNewDec(1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(0), math.LegacyNewDec(2)}, - {math.LegacyNewDec(-1), math.LegacyNewDec(1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(-1), math.LegacyNewDec(0), math.LegacyNewDec(-2)}, - - { - math.LegacyNewDec(3), math.LegacyNewDec(7), math.LegacyNewDec(21), math.LegacyNewDec(21), math.LegacyNewDec(21), - math.LegacyNewDecWithPrec(428571428571428571, 18), math.LegacyNewDecWithPrec(428571428571428572, 18), math.LegacyNewDecWithPrec(428571428571428571, 18), - math.LegacyNewDec(10), math.LegacyNewDec(-4), - }, - { - math.LegacyNewDec(2), math.LegacyNewDec(4), math.LegacyNewDec(8), math.LegacyNewDec(8), math.LegacyNewDec(8), math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDecWithPrec(5, 1), - math.LegacyNewDec(6), math.LegacyNewDec(-2), - }, - - {math.LegacyNewDec(100), math.LegacyNewDec(100), math.LegacyNewDec(10000), math.LegacyNewDec(10000), math.LegacyNewDec(10000), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(200), math.LegacyNewDec(0)}, - - { - math.LegacyNewDecWithPrec(15, 1), math.LegacyNewDecWithPrec(15, 1), math.LegacyNewDecWithPrec(225, 2), math.LegacyNewDecWithPrec(225, 2), math.LegacyNewDecWithPrec(225, 2), - math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(1), math.LegacyNewDec(3), math.LegacyNewDec(0), - }, - { - math.LegacyNewDecWithPrec(3333, 4), math.LegacyNewDecWithPrec(333, 4), math.LegacyNewDecWithPrec(1109889, 8), math.LegacyNewDecWithPrec(1109889, 8), math.LegacyNewDecWithPrec(1109889, 8), - math.LegacyMustNewDecFromStr("10.009009009009009009"), math.LegacyMustNewDecFromStr("10.009009009009009010"), math.LegacyMustNewDecFromStr("10.009009009009009009"), - math.LegacyNewDecWithPrec(3666, 4), math.LegacyNewDecWithPrec(3, 1), - }, - } +// Property: a * b == b * a +func testMulCommutative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") - for tcIndex, tc := range tests { - tc := tc - resAdd := tc.d1.Add(tc.d2) - resSub := tc.d1.Sub(tc.d2) - resMul := tc.d1.Mul(tc.d2) - resMulTruncate := tc.d1.MulTruncate(tc.d2) - resMulRoundUp := tc.d1.MulRoundUp(tc.d2) - s.Require().True(tc.expAdd.Equal(resAdd), "exp %v, res %v, tc %d", tc.expAdd, resAdd, tcIndex) - s.Require().True(tc.expSub.Equal(resSub), "exp %v, res %v, tc %d", tc.expSub, resSub, tcIndex) - s.Require().True(tc.expMul.Equal(resMul), "exp %v, res %v, tc %d", tc.expMul, resMul, tcIndex) - s.Require().True(tc.expMulTruncate.Equal(resMulTruncate), "exp %v, res %v, tc %d", tc.expMulTruncate, resMulTruncate, tcIndex) - s.Require().True(tc.expMulRoundUp.Equal(resMulRoundUp), "exp %v, res %v, tc %d", tc.expMulRoundUp, resMulRoundUp, tcIndex) - - if tc.d2.IsZero() { // panic for divide by zero - s.Require().Panics(func() { tc.d1.Quo(tc.d2) }) - } else { - resQuo := tc.d1.Quo(tc.d2) - s.Require().True(tc.expQuo.Equal(resQuo), "exp %v, res %v, tc %d", tc.expQuo.String(), resQuo.String(), tcIndex) + c, err := a.Mul(b) + require.NoError(t, err) - resQuoRoundUp := tc.d1.QuoRoundUp(tc.d2) - s.Require().True(tc.expQuoRoundUp.Equal(resQuoRoundUp), "exp %v, res %v, tc %d", - tc.expQuoRoundUp.String(), resQuoRoundUp.String(), tcIndex) + d, err := b.Mul(a) + require.NoError(t, err) - resQuoTruncate := tc.d1.QuoTruncate(tc.d2) - s.Require().True(tc.expQuoTruncate.Equal(resQuoTruncate), "exp %v, res %v, tc %d", - tc.expQuoTruncate.String(), resQuoTruncate.String(), tcIndex) - } - } + require.True(t, c.Equal(d)) } -func (s *decimalTestSuite) TestMulRoundUp_RoundingAtPrecisionEnd() { - var ( - a = math.LegacyMustNewDecFromStr("0.000000000000000009") - b = math.LegacyMustNewDecFromStr("0.000000000000000009") - expectedRoundUp = math.LegacyMustNewDecFromStr("0.000000000000000001") - expectedTruncate = math.LegacyMustNewDecFromStr("0.000000000000000000") - ) +// Property: (a * b) * c == a * (b * c) +func testMulAssociative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + c := genDec.Draw(t, "c") - actualRoundUp := a.MulRoundUp(b) - s.Require().Equal(expectedRoundUp.String(), actualRoundUp.String(), "exp %v, res %v", expectedRoundUp, actualRoundUp) + // (a * b) * c + d, err := a.Mul(b) + require.NoError(t, err) + + e, err := d.Mul(c) + require.NoError(t, err) + + // a * (b * c) + f, err := b.Mul(c) + require.NoError(t, err) - actualTruncate := a.MulTruncate(b) - s.Require().Equal(expectedTruncate.String(), actualTruncate.String(), "exp %v, res %v", expectedRoundUp, actualTruncate) + g, err := a.Mul(f) + require.NoError(t, err) + + require.True(t, e.Equal(g)) } -func (s *decimalTestSuite) TestBankerRoundChop() { - tests := []struct { - d1 math.LegacyDec - exp int64 - }{ - {s.mustNewDecFromStr("0.25"), 0}, - {s.mustNewDecFromStr("0"), 0}, - {s.mustNewDecFromStr("1"), 1}, - {s.mustNewDecFromStr("0.75"), 1}, - {s.mustNewDecFromStr("0.5"), 0}, - {s.mustNewDecFromStr("7.5"), 8}, - {s.mustNewDecFromStr("1.5"), 2}, - {s.mustNewDecFromStr("2.5"), 2}, - {s.mustNewDecFromStr("0.545"), 1}, // 0.545-> 1 even though 5 is first decimal and 1 not even - {s.mustNewDecFromStr("1.545"), 2}, - } +// Property: (a - b) + b == a +func testSubAdd(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") - for tcIndex, tc := range tests { - resNeg := tc.d1.Neg().RoundInt64() - s.Require().Equal(-1*tc.exp, resNeg, "negative tc %d", tcIndex) + c, err := a.Sub(b) + require.NoError(t, err) - resPos := tc.d1.RoundInt64() - s.Require().Equal(tc.exp, resPos, "positive tc %d", tcIndex) - } + d, err := c.Add(b) + require.NoError(t, err) + + require.True(t, a.Equal(d)) } -func (s *decimalTestSuite) TestTruncate() { - tests := []struct { - d1 math.LegacyDec - exp int64 - }{ - {s.mustNewDecFromStr("0"), 0}, - {s.mustNewDecFromStr("0.25"), 0}, - {s.mustNewDecFromStr("0.75"), 0}, - {s.mustNewDecFromStr("1"), 1}, - {s.mustNewDecFromStr("1.5"), 1}, - {s.mustNewDecFromStr("7.5"), 7}, - {s.mustNewDecFromStr("7.6"), 7}, - {s.mustNewDecFromStr("7.4"), 7}, - {s.mustNewDecFromStr("100.1"), 100}, - {s.mustNewDecFromStr("1000.1"), 1000}, - } +// Property: (a + b) - b == a +func testAddSub(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") - for tcIndex, tc := range tests { - resNeg := tc.d1.Neg().TruncateInt64() - s.Require().Equal(-1*tc.exp, resNeg, "negative tc %d", tcIndex) + c, err := a.Add(b) + require.NoError(t, err) - resPos := tc.d1.TruncateInt64() - s.Require().Equal(tc.exp, resPos, "positive tc %d", tcIndex) - } + d, err := c.Sub(b) + require.NoError(t, err) + + require.True(t, a.Equal(d)) } -func (s *decimalTestSuite) TestStringOverflow() { - // two random 64 bit primes - dec1, err := math.LegacyNewDecFromStr("51643150036226787134389711697696177267") - s.Require().NoError(err) - dec2, err := math.LegacyNewDecFromStr("-31798496660535729618459429845579852627") - s.Require().NoError(err) - dec3 := dec1.Add(dec2) - s.Require().Equal( - "19844653375691057515930281852116324640.000000000000000000", - dec3.String(), - ) -} - -func (s *decimalTestSuite) TestDecMulInt() { - tests := []struct { - sdkDec math.LegacyDec - sdkInt math.Int - want math.LegacyDec - }{ - {math.LegacyNewDec(10), math.NewInt(2), math.LegacyNewDec(20)}, - {math.LegacyNewDec(1000000), math.NewInt(100), math.LegacyNewDec(100000000)}, - {math.LegacyNewDecWithPrec(1, 1), math.NewInt(10), math.LegacyNewDec(1)}, - {math.LegacyNewDecWithPrec(1, 5), math.NewInt(20), math.LegacyNewDecWithPrec(2, 4)}, - } - for i, tc := range tests { - got := tc.sdkDec.MulInt(tc.sdkInt) - s.Require().Equal(tc.want, got, "Incorrect result on test case %d", i) - } +// Property: a * 0 = 0 +func testMulZero(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := Dec{} + + c, err := a.Mul(zero) + require.NoError(t, err) + require.True(t, c.IsZero()) } -func (s *decimalTestSuite) TestDecCeil() { - testCases := []struct { - input math.LegacyDec - expected math.LegacyDec - }{ - {math.LegacyNewDecWithPrec(1000000000000000, math.LegacyPrecision), math.LegacyNewDec(1)}, // 0.001 => 1.0 - {math.LegacyNewDecWithPrec(-1000000000000000, math.LegacyPrecision), math.LegacyZeroDec()}, // -0.001 => 0.0 - {math.LegacyZeroDec(), math.LegacyZeroDec()}, // 0.0 => 0.0 - {math.LegacyNewDecWithPrec(900000000000000000, math.LegacyPrecision), math.LegacyNewDec(1)}, // 0.9 => 1.0 - {math.LegacyNewDecWithPrec(4001000000000000000, math.LegacyPrecision), math.LegacyNewDec(5)}, // 4.001 => 5.0 - {math.LegacyNewDecWithPrec(-4001000000000000000, math.LegacyPrecision), math.LegacyNewDec(-4)}, // -4.001 => -4.0 - {math.LegacyNewDecWithPrec(4700000000000000000, math.LegacyPrecision), math.LegacyNewDec(5)}, // 4.7 => 5.0 - {math.LegacyNewDecWithPrec(-4700000000000000000, math.LegacyPrecision), math.LegacyNewDec(-4)}, // -4.7 => -4.0 - } +// Property: a/a = 1 +func testSelfQuo(t *rapid.T) { + decNotZero := func(d Dec) bool { return !d.IsZero() } + a := genDec.Filter(decNotZero).Draw(t, "a") + one := NewDecFromInt64(1) - for i, tc := range testCases { - res := tc.input.Ceil() - s.Require().Equal(tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) - } + b, err := a.Quo(a) + require.NoError(t, err) + require.True(t, one.Equal(b)) } -func (s *decimalTestSuite) TestCeilOverflow() { - d, err := math.LegacyNewDecFromStr("66749594872528440074844428317798503581334516323645399060845050244444366430645.000000000000000001") - s.Require().NoError(err) - s.Require().True(d.BigInt().BitLen() <= 315, "d is too large") - // this call panics because the value is too large - s.Require().Panics(func() { d.Ceil() }, "Ceil should panic on overflow") +// Property: a/1 = a +func testQuoByOne(t *rapid.T) { + a := genDec.Draw(t, "a") + one := NewDecFromInt64(1) + + b, err := a.Quo(one) + require.NoError(t, err) + require.True(t, a.Equal(b)) } -func (s *decimalTestSuite) TestPower() { - testCases := []struct { - input math.LegacyDec - power uint64 - expected math.LegacyDec - }{ - {math.LegacyNewDec(100), 0, math.LegacyOneDec()}, // 10 ^ (0) => 1.0 - {math.LegacyOneDec(), 10, math.LegacyOneDec()}, // 1.0 ^ (10) => 1.0 - {math.LegacyNewDecWithPrec(5, 1), 2, math.LegacyNewDecWithPrec(25, 2)}, // 0.5 ^ 2 => 0.25 - {math.LegacyNewDecWithPrec(2, 1), 2, math.LegacyNewDecWithPrec(4, 2)}, // 0.2 ^ 2 => 0.04 - {math.LegacyNewDecFromInt(math.NewInt(3)), 3, math.LegacyNewDecFromInt(math.NewInt(27))}, // 3 ^ 3 => 27 - {math.LegacyNewDecFromInt(math.NewInt(-3)), 4, math.LegacyNewDecFromInt(math.NewInt(81))}, // -3 ^ 4 = 81 - {math.LegacyNewDecWithPrec(1414213562373095049, 18), 2, math.LegacyNewDecFromInt(math.NewInt(2))}, // 1.414213562373095049 ^ 2 = 2 - } +// Property: (a * b) / a == b +func testMulQuoA(t *rapid.T) { + decNotZero := func(d Dec) bool { return !d.IsZero() } + a := genDec.Filter(decNotZero).Draw(t, "a") + b := genDec.Draw(t, "b") - for i, tc := range testCases { - res := tc.input.Power(tc.power) - s.Require().True(tc.expected.Sub(res).Abs().LTE(math.LegacySmallestDec()), "unexpected result for test case %d, normal power, input: %v", i, tc.input) + c, err := a.Mul(b) + require.NoError(t, err) - mutableInput := tc.input - mutableInput.PowerMut(tc.power) - s.Require().True(tc.expected.Sub(mutableInput).Abs().LTE(math.LegacySmallestDec()), - "unexpected result for test case %d, input %v", i, tc.input) - s.Require().True(res.Equal(tc.input), "unexpected result for test case %d, mutable power, input: %v", i, tc.input) - } + d, err := c.Quo(a) + require.NoError(t, err) + + require.True(t, b.Equal(d)) } -func (s *decimalTestSuite) TestApproxRoot() { - testCases := []struct { - input math.LegacyDec - root uint64 - expected math.LegacyDec - }{ - {math.LegacyOneDec(), 10, math.LegacyOneDec()}, // 1.0 ^ (0.1) => 1.0 - {math.LegacyNewDecWithPrec(25, 2), 2, math.LegacyNewDecWithPrec(5, 1)}, // 0.25 ^ (0.5) => 0.5 - {math.LegacyNewDecWithPrec(4, 2), 2, math.LegacyNewDecWithPrec(2, 1)}, // 0.04 ^ (0.5) => 0.2 - {math.LegacyNewDecFromInt(math.NewInt(27)), 3, math.LegacyNewDecFromInt(math.NewInt(3))}, // 27 ^ (1/3) => 3 - {math.LegacyNewDecFromInt(math.NewInt(-81)), 4, math.LegacyNewDecFromInt(math.NewInt(-3))}, // -81 ^ (0.25) => -3 - {math.LegacyNewDecFromInt(math.NewInt(2)), 2, math.LegacyNewDecWithPrec(1414213562373095049, 18)}, // 2 ^ (0.5) => 1.414213562373095049 - {math.LegacyNewDecWithPrec(1005, 3), 31536000, math.LegacyMustNewDecFromStr("1.000000000158153904")}, // 1.005 ^ (1/31536000) ≈ 1.00000000016 - {math.LegacySmallestDec(), 2, math.LegacyNewDecWithPrec(1, 9)}, // 1e-18 ^ (0.5) => 1e-9 - {math.LegacySmallestDec(), 3, math.LegacyMustNewDecFromStr("0.000000999999999997")}, // 1e-18 ^ (1/3) => 1e-6 - {math.LegacyNewDecWithPrec(1, 8), 3, math.LegacyMustNewDecFromStr("0.002154434690031900")}, // 1e-8 ^ (1/3) ≈ 0.00215443469 - {math.LegacyMustNewDecFromStr("9000002314687921634000000000000000000021394871242000000000000000"), 2, math.LegacyMustNewDecFromStr("94868342004527103646332858502867.899477053226766107")}, - } +// Property: (a * b) / b == a +func testMulQuoB(t *rapid.T) { + decNotZero := func(d Dec) bool { return !d.IsZero() } + a := genDec.Draw(t, "a") + b := genDec.Filter(decNotZero).Draw(t, "b") - // In the case of 1e-8 ^ (1/3), the result repeats every 5 iterations starting from iteration 24 - // (i.e. 24, 29, 34, ... give the same result) and never converges enough. The maximum number of - // iterations (300) causes the result at iteration 300 to be returned, regardless of convergence. + c, err := a.Mul(b) + require.NoError(t, err) - for i, tc := range testCases { - res, err := tc.input.ApproxRoot(tc.root) - s.Require().NoError(err) - s.Require().True(tc.expected.Sub(res).Abs().LTE(math.LegacySmallestDec()), "unexpected result for test case %d, input: %v", i, tc.input) - } + d, err := c.Quo(b) + require.NoError(t, err) + + require.True(t, a.Equal(d)) } -func (s *decimalTestSuite) TestApproxSqrt() { - testCases := []struct { - input math.LegacyDec - expected math.LegacyDec - }{ - {math.LegacyOneDec(), math.LegacyOneDec()}, // 1.0 => 1.0 - {math.LegacyNewDecWithPrec(25, 2), math.LegacyNewDecWithPrec(5, 1)}, // 0.25 => 0.5 - {math.LegacyNewDecWithPrec(4, 2), math.LegacyNewDecWithPrec(2, 1)}, // 0.09 => 0.3 - {math.LegacyNewDec(9), math.LegacyNewDecFromInt(math.NewInt(3))}, // 9 => 3 - {math.LegacyNewDec(-9), math.LegacyNewDecFromInt(math.NewInt(-3))}, // -9 => -3 - {math.LegacyNewDec(2), math.LegacyNewDecWithPrec(1414213562373095049, 18)}, // 2 => 1.414213562373095049 - { // 2^127 - 1 => 13043817825332782212.3495718062525083688 which rounds to 13043817825332782212.3495718062525083689 - math.LegacyNewDec(2).Power(127).Sub(math.LegacyOneDec()), - math.LegacyMustNewDecFromStr("13043817825332782212.349571806252508369"), - }, - {math.LegacyMustNewDecFromStr("1.000000011823380862"), math.LegacyMustNewDecFromStr("1.000000005911690414")}, - } +// Property: (a * 10^b) / 10^b == a using MulExact and QuoExact +// and a with no more than b decimal places (b <= 32). +func testMulQuoExact(t *rapid.T) { + b := rapid.Uint32Range(0, 32).Draw(t, "b") + decPrec := func(d Dec) bool { return d.NumDecimalPlaces() <= b } + a := genDec.Filter(decPrec).Draw(t, "a") - for i, tc := range testCases { - res, err := tc.input.ApproxSqrt() - s.Require().NoError(err) - s.Require().Equal(tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) - } -} + c := NewDecFinite(1, int32(b)) -func (s *decimalTestSuite) TestDecSortableBytes() { - tests := []struct { - d math.LegacyDec - want []byte - }{ - {math.LegacyNewDec(0), []byte("000000000000000000.000000000000000000")}, - {math.LegacyNewDec(1), []byte("000000000000000001.000000000000000000")}, - {math.LegacyNewDec(10), []byte("000000000000000010.000000000000000000")}, - {math.LegacyNewDec(12340), []byte("000000000000012340.000000000000000000")}, - {math.LegacyNewDecWithPrec(12340, 4), []byte("000000000000000001.234000000000000000")}, - {math.LegacyNewDecWithPrec(12340, 5), []byte("000000000000000000.123400000000000000")}, - {math.LegacyNewDecWithPrec(12340, 8), []byte("000000000000000000.000123400000000000")}, - {math.LegacyNewDecWithPrec(1009009009009009009, 17), []byte("000000000000000010.090090090090090090")}, - {math.LegacyNewDecWithPrec(-1009009009009009009, 17), []byte("-000000000000000010.090090090090090090")}, - {math.LegacyNewDec(1000000000000000000), []byte("max")}, - {math.LegacyNewDec(-1000000000000000000), []byte("--")}, - } - for tcIndex, tc := range tests { - s.Require().Equal(tc.want, math.LegacySortableDecBytes(tc.d), "bad String(), index: %v", tcIndex) - } + d, err := a.MulExact(c) + require.NoError(t, err) - s.Require().Panics(func() { math.LegacySortableDecBytes(math.LegacyNewDec(1000000000000000001)) }) - s.Require().Panics(func() { math.LegacySortableDecBytes(math.LegacyNewDec(-1000000000000000001)) }) -} + e, err := d.QuoExact(c) + require.NoError(t, err) -func (s *decimalTestSuite) TestDecEncoding() { - largestBigInt, ok := new(big.Int).SetString("33499189745056880149688856635597007162669032647290798121690100488888732861290034376435130433535", 10) - s.Require().True(ok) + require.True(t, a.Equal(e)) +} - smallestBigInt, ok := new(big.Int).SetString("-33499189745056880149688856635597007162669032647290798121690100488888732861290034376435130433535", 10) - s.Require().True(ok) +// Property: (a / b) * b == a using QuoExact and MulExact and +// a as an integer. +func testQuoMulExact(t *rapid.T) { + a := rapid.Uint64().Draw(t, "a") + aDec, err := NewDecFromString(fmt.Sprintf("%d", a)) + require.NoError(t, err) + b := rapid.Uint32Range(0, 32).Draw(t, "b") + c := NewDecFinite(1, int32(b)) - const maxDecBitLen = 315 - maxInt, ok := new(big.Int).SetString(strings.Repeat("1", maxDecBitLen), 2) - s.Require().True(ok) + require.NoError(t, err) - testCases := []struct { - input math.LegacyDec - rawBz string - jsonStr string - yamlStr string - }{ - { - math.LegacyNewDec(0), "30", - "\"0.000000000000000000\"", - "\"0.000000000000000000\"\n", - }, - { - math.LegacyNewDecWithPrec(4, 2), - "3430303030303030303030303030303030", - "\"0.040000000000000000\"", - "\"0.040000000000000000\"\n", - }, - { - math.LegacyNewDecWithPrec(-4, 2), - "2D3430303030303030303030303030303030", - "\"-0.040000000000000000\"", - "\"-0.040000000000000000\"\n", - }, - { - math.LegacyNewDecWithPrec(1414213562373095049, 18), - "31343134323133353632333733303935303439", - "\"1.414213562373095049\"", - "\"1.414213562373095049\"\n", - }, - { - math.LegacyNewDecWithPrec(-1414213562373095049, 18), - "2D31343134323133353632333733303935303439", - "\"-1.414213562373095049\"", - "\"-1.414213562373095049\"\n", - }, - { - math.LegacyNewDecFromBigIntWithPrec(largestBigInt, 18), - "3333343939313839373435303536383830313439363838383536363335353937303037313632363639303332363437323930373938313231363930313030343838383838373332383631323930303334333736343335313330343333353335", - "\"33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535\"", - "\"33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535\"\n", - }, - { - math.LegacyNewDecFromBigIntWithPrec(smallestBigInt, 18), - "2D3333343939313839373435303536383830313439363838383536363335353937303037313632363639303332363437323930373938313231363930313030343838383838373332383631323930303334333736343335313330343333353335", - "\"-33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535\"", - "\"-33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535\"\n", - }, - { - math.LegacyNewDecFromBigIntWithPrec(maxInt, 18), - "3636373439353934383732353238343430303734383434343238333137373938353033353831333334353136333233363435333939303630383435303530323434343434333636343330363435303137313838323137353635323136373637", - "\"66749594872528440074844428317798503581334516323645399060845050244444366430645.017188217565216767\"", - "\"66749594872528440074844428317798503581334516323645399060845050244444366430645.017188217565216767\"\n", - }, - } + d, err := aDec.QuoExact(c) + require.NoError(t, err) - for _, tc := range testCases { - bz, err := tc.input.Marshal() - s.Require().NoError(err) - s.Require().Equal(tc.rawBz, fmt.Sprintf("%X", bz)) + e, err := d.MulExact(c) + require.NoError(t, err) - var other math.LegacyDec - s.Require().NoError((&other).Unmarshal(bz)) - s.Require().True(tc.input.Equal(other)) + require.True(t, aDec.Equal(e)) +} - bz, err = json.Marshal(tc.input) - s.Require().NoError(err) - s.Require().Equal(tc.jsonStr, string(bz)) - s.Require().NoError(json.Unmarshal(bz, &other)) - s.Require().True(tc.input.Equal(other)) +// Property: Cmp(a, b) == -Cmp(b, a) +func testCmpInverse(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") - bz, err = yaml.Marshal(tc.input) - s.Require().NoError(err) - s.Require().Equal(tc.yamlStr, string(bz)) - } + require.Equal(t, a.Cmp(b), -b.Cmp(a)) } -// Showcase that different orders of operations causes different results. -func (s *decimalTestSuite) TestOperationOrders() { - n1 := math.LegacyNewDec(10) - n2 := math.LegacyNewDec(1000000010) - s.Require().Equal(n1.Mul(n2).Quo(n2), math.LegacyNewDec(10)) - s.Require().NotEqual(n1.Mul(n2).Quo(n2), n1.Quo(n2).Mul(n2)) +// Property: Equal(a, b) == Equal(b, a) +func testEqualCommutative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + + require.Equal(t, a.Equal(b), b.Equal(a)) } -func BenchmarkMarshalTo(b *testing.B) { - b.ReportAllocs() - bis := []struct { - in math.LegacyDec - want []byte - }{ - { - math.LegacyNewDec(1e8), []byte{ - 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, - }, - }, - {math.LegacyNewDec(0), []byte{0x30}}, - } - data := make([]byte, 100) - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - for _, bi := range bis { - if n, err := bi.in.MarshalTo(data); err != nil { - b.Fatal(err) - } else if !bytes.Equal(data[:n], bi.want) { - b.Fatalf("Mismatch\nGot: % x\nWant: % x\n", data[:n], bi.want) - } - } - } +// Property: isZero(f) == isZero(NewDecFromString(f.String())) +func testIsZero(t *rapid.T) { + floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") + f, dec := floatAndDec.float, floatAndDec.dec + + require.Equal(t, f == 0, dec.IsZero()) + } -var sink interface{} +// Property: isNegative(f) == isNegative(NewDecFromString(f.String())) +func testIsNegative(t *rapid.T) { + floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") + f, dec := floatAndDec.float, floatAndDec.dec -func BenchmarkLegacyQuoMut(b *testing.B) { - b1 := math.LegacyNewDec(17e2 + 8371) - b2 := math.LegacyNewDec(4371) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - sink = b1.QuoMut(b2) - } + require.Equal(t, f < 0, dec.IsNegative()) +} - if sink == nil { - b.Fatal("Benchmark did not run") - } - sink = (interface{})(nil) +// Property: isPositive(f) == isPositive(NewDecFromString(f.String())) +func testIsPositive(t *rapid.T) { + floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") + f, dec := floatAndDec.float, floatAndDec.dec + + require.Equal(t, f > 0, dec.IsPositive()) } -func BenchmarkLegacyQuoTruncateMut(b *testing.B) { - b1 := math.LegacyNewDec(17e2 + 8371) - b2 := math.LegacyNewDec(4371) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - sink = b1.QuoTruncateMut(b2) - } +// Property: floatDecimalPlaces(f) == NumDecimalPlaces(NewDecFromString(f.String())) +func testNumDecimalPlaces(t *rapid.T) { + floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") + f, dec := floatAndDec.float, floatAndDec.dec - if sink == nil { - b.Fatal("Benchmark did not run") - } - sink = (interface{})(nil) + require.Equal(t, floatDecimalPlaces(t, f), dec.NumDecimalPlaces()) } -func BenchmarkLegacySqrtOnMersennePrime(b *testing.B) { - b1 := math.LegacyNewDec(2).Power(127).Sub(math.LegacyOneDec()) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - sink, _ = b1.ApproxSqrt() +func floatDecimalPlaces(t *rapid.T, f float64) uint32 { + reScientific := regexp.MustCompile(`^\-?(?:[[:digit:]]+(?:\.([[:digit:]]+))?|\.([[:digit:]]+))(?:e?(?:\+?([[:digit:]]+)|(-[[:digit:]]+)))?$`) + fStr := fmt.Sprintf("%g", f) + matches := reScientific.FindAllStringSubmatch(fStr, 1) + if len(matches) != 1 { + t.Fatalf("Didn't match float: %g", f) } - if sink == nil { - b.Fatal("Benchmark did not run") + // basePlaces is the number of decimal places in the decimal part of the + // string + basePlaces := 0 + if matches[0][1] != "" { + basePlaces = len(matches[0][1]) + } else if matches[0][2] != "" { + basePlaces = len(matches[0][2]) } - sink = (interface{})(nil) -} + t.Logf("Base places: %d", basePlaces) -func BenchmarkLegacyQuoRoundupMut(b *testing.B) { - b1 := math.LegacyNewDec(17e2 + 8371) - b2 := math.LegacyNewDec(4371) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - sink = b1.QuoRoundupMut(b2) + // exp is the exponent + exp := 0 + if matches[0][3] != "" { + var err error + exp, err = strconv.Atoi(matches[0][3]) + require.NoError(t, err) + } else if matches[0][4] != "" { + var err error + exp, err = strconv.Atoi(matches[0][4]) + require.NoError(t, err) } - if sink == nil { - b.Fatal("Benchmark did not run") + // Subtract exponent from base and check if negative + res := basePlaces - exp + if res <= 0 { + return 0 } - sink = (interface{})(nil) + + return uint32(res) } -func TestFormatDec(t *testing.T) { - type decimalTest []string - var testcases []decimalTest - raw, err := os.ReadFile("./testdata/decimals.json") +func TestIsFinite(t *testing.T) { + a, err := NewDecFromString("1.5") require.NoError(t, err) - err = json.Unmarshal(raw, &testcases) + + require.True(t, a.IsFinite()) + + b, err := NewDecFromString("NaN") require.NoError(t, err) - for _, tc := range testcases { - tc := tc - t.Run(tc[0], func(t *testing.T) { - out, err := math.FormatDec(tc[0]) - require.NoError(t, err) - require.Equal(t, tc[1], out) - }) - } + require.False(t, b.IsFinite()) } -func TestFormatDecNonDigits(t *testing.T) { - badCases := []string{ - "10.a", - "1a.10", - "p1a10.", - "0.10p", - "--10", - "12.😎😎", - "11111111111133333333333333333333333333333a", - "11111111111133333333333333333333333333333 192892", - } +func TestReduce(t *testing.T) { + a, err := NewDecFromString("1.30000") + require.NoError(t, err) + b, n := a.Reduce() + require.Equal(t, 4, n) + require.True(t, a.Equal(b)) + require.Equal(t, "1.3", b.String()) +} - for _, value := range badCases { - value := value - t.Run(value, func(t *testing.T) { - s, err := math.FormatDec(value) - if err == nil { - t.Fatal("Expected an error") - } - if g, w := err.Error(), "non-digits"; !strings.Contains(g, w) { - t.Errorf("Error mismatch\nGot: %q\nWant substring: %q", g, w) - } - if s != "" { - t.Fatalf("Got a non-empty string: %q", s) - } - }) - } +func TestMulExactGood(t *testing.T) { + a, err := NewDecFromString("1.000001") + require.NoError(t, err) + b := NewDecFinite(1, 6) + c, err := a.MulExact(b) + require.NoError(t, err) + d, err := c.Int64() + require.NoError(t, err) + require.Equal(t, int64(1000001), d) } -func TestNegativePrecisionPanic(t *testing.T) { - require.Panics(t, func() { - math.LegacyNewDecWithPrec(10, -1) - }) +func TestMulExactBad(t *testing.T) { + a, err := NewDecFromString("1.000000000000000000000000000000000000123456789") + require.NoError(t, err) + b := NewDecFinite(1, 10) + _, err = a.MulExact(b) + require.ErrorIs(t, err, ErrUnexpectedRounding) } -func (s *decimalTestSuite) TestConvertToBigIntMutativeForLegacyDec() { - r := big.NewInt(30) - i := math.LegacyNewDecFromBigInt(r) +func TestQuoExactGood(t *testing.T) { + a, err := NewDecFromString("1000001") + require.NoError(t, err) + b := NewDecFinite(1, 6) + c, err := a.QuoExact(b) + require.NoError(t, err) + require.Equal(t, "1.000001", c.String()) +} - // Compare value of BigInt & BigIntMut - s.Require().Equal(i.BigInt(), i.BigIntMut()) +func TestQuoExactBad(t *testing.T) { + a, err := NewDecFromString("1000000000000000000000000000000000000123456789") + require.NoError(t, err) + b := NewDecFinite(1, 10) + _, err = a.QuoExact(b) + require.ErrorIs(t, err, ErrUnexpectedRounding) +} - // Modify BigIntMut() pointer and ensure i.BigIntMut() & i.BigInt() change - p1 := i.BigIntMut() - p1.SetInt64(40) - s.Require().Equal(big.NewInt(40), i.BigIntMut()) - s.Require().Equal(big.NewInt(40), i.BigInt()) +func TestToBigInt(t *testing.T) { + i1 := "1000000000000000000000000000000000000123456789" + tcs := []struct { + intStr string + out string + isError error + }{ + {i1, i1, nil}, + {"1000000000000000000000000000000000000123456789.00000000", i1, nil}, + {"123.456e6", "123456000", nil}, + {"12345.6", "", ErrNonIntegeral}, + } + 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) + } + } +} - // Modify big.Int() pointer and ensure i.BigIntMut() & i.BigInt() don't change - p2 := i.BigInt() - p2.SetInt64(50) - s.Require().NotEqual(big.NewInt(50), i.BigIntMut()) - s.Require().NotEqual(big.NewInt(50), i.BigInt()) +func TestToSdkInt(t *testing.T) { + i1 := "1000000000000000000000000000000000000123456789" + tcs := []struct { + intStr string + out string + }{ + {i1, i1}, + {"1000000000000000000000000000000000000123456789.00000000", i1}, + {"123.456e6", "123456000"}, + {"123.456e1", "1234"}, + {"123.456", "123"}, + {"123.956", "123"}, + {"-123.456", "-123"}, + {"-123.956", "-123"}, + {"-0.956", "0"}, + {"-0.9", "0"}, + } + for idx, tc := range tcs { + a, err := NewDecFromString(tc.intStr) + require.NoError(t, err) + b := a.SdkIntTrim() + require.Equal(t, tc.out, b.String(), "test_%d", idx) + } +} + +func TestInfDecString(t *testing.T) { + _, err := NewDecFromString("iNf") + require.Error(t, err) + require.ErrorIs(t, err, ErrInfiniteString) } diff --git a/math/go.mod b/math/go.mod index 53100ea6e7bb..a2ae2365e42c 100644 --- a/math/go.mod +++ b/math/go.mod @@ -1,19 +1,43 @@ module cosmossdk.io/math -go 1.20 +go 1.21 + +toolchain go1.21.5 require ( github.com/stretchr/testify v1.9.0 - golang.org/x/exp v0.0.0-20221205204356-47842c84f3db + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 sigs.k8s.io/yaml v1.4.0 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/cometbft/cometbft v0.38.5 // indirect + github.com/cosmos/gogoproto v1.4.11 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect + github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sasha-s/go-deadlock v0.3.1 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect + google.golang.org/grpc v1.62.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) + +require ( + cosmossdk.io/errors v1.0.1 + github.com/cockroachdb/apd/v2 v2.0.2 + github.com/cosmos/cosmos-sdk v0.50.5 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + pgregory.net/rapid v1.1.0 ) // Issue with math.Int{}.Size() implementation. diff --git a/math/go.sum b/math/go.sum index a6d9afc96b85..9028e67ed6d9 100644 --- a/math/go.sum +++ b/math/go.sum @@ -1,8 +1,34 @@ +cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= +cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= +github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= +github.com/cometbft/cometbft v0.38.5 h1:4lOcK5VTPrfbLOhNHmPYe6c7eDXHtBdMCQuKbAfFJdU= +github.com/cometbft/cometbft v0.38.5/go.mod h1:0tqKin+KQs8zDwzYD8rPHzSBIDNPuB4NrwwGDNb/hUg= +github.com/cosmos/cosmos-sdk v0.50.5 h1:MOEi+DKYgW67YaPgB+Pf+nHbD3V9S/ayitRKJYLfGIA= +github.com/cosmos/cosmos-sdk v0.50.5/go.mod h1:oV/k6GJgXV9QPoM2fsYDPPsyPBgQbdotv532O6Mz1OQ= +github.com/cosmos/gogoproto v1.4.11 h1:LZcMHrx4FjUgrqQSWeaGC1v/TeuVFqSLa43CC6aWR2g= +github.com/cosmos/gogoproto v1.4.11/go.mod h1:/g39Mh8m17X8Q/GDEs5zYTSNaNnInBSohtaxzQnYq1Y= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -10,19 +36,61 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc h1:8bQZVK1X6BJR/6nYUPxQEP+ReTsceJTKizeuwjWOPUA= +github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= +google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/math/math.go b/math/math.go new file mode 100644 index 000000000000..43358b2c2bfd --- /dev/null +++ b/math/math.go @@ -0,0 +1,73 @@ +// Package math provides helper functions for doing mathematical calculations and parsing for the ecocredit module. +package math + +import ( + "fmt" + + "cosmossdk.io/errors" + "github.com/cockroachdb/apd/v2" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var exactContext = apd.Context{ + Precision: 0, + MaxExponent: apd.MaxExponent, + MinExponent: apd.MinExponent, + Traps: apd.DefaultTraps | apd.Inexact | apd.Rounded, +} + +// Add adds x and y +func Add(x Dec, y Dec) (Dec, error) { + return x.Add(y) +} + +// SubNonNegative subtracts the value of y from x and returns the result with +// arbitrary precision. Returns an error if the result is negative. +func SubNonNegative(x Dec, y Dec) (Dec, error) { + z, err := x.Sub(y) + if err != nil { + return Dec{}, err + } + + if z.IsNegative() { + return z, fmt.Errorf("result negative during non-negative subtraction") + } + + return z, nil +} + +// SafeSubBalance subtracts the value of y from x and returns the result with arbitrary precision. +// Returns with ErrInsufficientFunds error if the result is negative. +func SafeSubBalance(x Dec, y Dec) (Dec, error) { + var z Dec + _, err := exactContext.Sub(&z.dec, &x.dec, &y.dec) + if err != nil { + return z, errors.Wrap(err, "decimal subtraction error") + } + + if z.IsNegative() { + return z, sdkerrors.ErrInsufficientFunds + } + + return z, nil +} + +// SafeAddBalance adds the value of x+y and returns the result with arbitrary precision. +// Returns with ErrInvalidRequest error if either x or y is negative. +func SafeAddBalance(x Dec, y Dec) (Dec, error) { + var z Dec + + if x.IsNegative() || y.IsNegative() { + return z, errors.Wrap( + sdkerrors.ErrInvalidRequest, + fmt.Sprintf("AddBalance() requires two non-negative Dec parameters, but received %s and %s", x, y)) + } + + _, err := exactContext.Add(&z.dec, &x.dec, &y.dec) + if err != nil { + return z, errors.Wrap(err, "decimal subtraction error") + } + + return z, nil +} From 561a13ff765fe8959abf187faf765227a9f26bb7 Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 18 Apr 2024 13:51:26 +0200 Subject: [PATCH 02/22] fix version --- math/go.mod | 2 +- math/go.sum | 42 ++++++++++-------------------------------- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/math/go.mod b/math/go.mod index a2ae2365e42c..4130dd995dd5 100644 --- a/math/go.mod +++ b/math/go.mod @@ -2,7 +2,7 @@ module cosmossdk.io/math go 1.21 -toolchain go1.21.5 +toolchain go1.22.2 require ( github.com/stretchr/testify v1.9.0 diff --git a/math/go.sum b/math/go.sum index 9028e67ed6d9..b80eccfffbe9 100644 --- a/math/go.sum +++ b/math/go.sum @@ -2,8 +2,10 @@ cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= +github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cometbft/cometbft v0.38.5 h1:4lOcK5VTPrfbLOhNHmPYe6c7eDXHtBdMCQuKbAfFJdU= @@ -12,77 +14,53 @@ github.com/cosmos/cosmos-sdk v0.50.5 h1:MOEi+DKYgW67YaPgB+Pf+nHbD3V9S/ayitRKJYLf github.com/cosmos/cosmos-sdk v0.50.5/go.mod h1:oV/k6GJgXV9QPoM2fsYDPPsyPBgQbdotv532O6Mz1OQ= github.com/cosmos/gogoproto v1.4.11 h1:LZcMHrx4FjUgrqQSWeaGC1v/TeuVFqSLa43CC6aWR2g= github.com/cosmos/gogoproto v1.4.11/go.mod h1:/g39Mh8m17X8Q/GDEs5zYTSNaNnInBSohtaxzQnYq1Y= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc h1:8bQZVK1X6BJR/6nYUPxQEP+ReTsceJTKizeuwjWOPUA= github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From e29f917f72d8a79f2210db6f6249fe5a44d699fb Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 18 Apr 2024 20:22:32 +0200 Subject: [PATCH 03/22] Update go.mod --- math/go.mod | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/math/go.mod b/math/go.mod index 4130dd995dd5..0f897d363421 100644 --- a/math/go.mod +++ b/math/go.mod @@ -1,8 +1,6 @@ module cosmossdk.io/math -go 1.21 - -toolchain go1.22.2 +go 1.20 require ( github.com/stretchr/testify v1.9.0 From 115ecc4e2768840a36bec6ac155a6f8c97276a01 Mon Sep 17 00:00:00 2001 From: samricotta Date: Mon, 22 Apr 2024 10:12:25 +0200 Subject: [PATCH 04/22] Update to docs --- docs/architecture/adr-069-gov-improvements.md | 2 +- x/distribution/README.md | 2 +- x/mint/README.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/architecture/adr-069-gov-improvements.md b/docs/architecture/adr-069-gov-improvements.md index af5b12645205..7b156f21c36c 100644 --- a/docs/architecture/adr-069-gov-improvements.md +++ b/docs/architecture/adr-069-gov-improvements.md @@ -172,7 +172,7 @@ type CalculateVoteResultsAndVotingPowerFn func( keeper Keeper, proposalID uint64, validators map[string]v1.ValidatorGovInfo, -) (totalVoterPower math.LegacyDec, results map[v1.VoteOption]math.LegacyDec, err error) +) (totalVoterPower math.Dec, results map[v1.VoteOption]math.Dec, err error) ``` ## Consequences diff --git a/x/distribution/README.md b/x/distribution/README.md index 03e1e5baff21..002e15ed3c78 100644 --- a/x/distribution/README.md +++ b/x/distribution/README.md @@ -139,7 +139,7 @@ Once those rewards are big enough, they are sent as `sdk.Coins` to the community type DecCoins []DecCoin type DecCoin struct { - Amount math.LegacyDec + Amount math.Dec Denom string } ``` diff --git a/x/mint/README.md b/x/mint/README.md index 80198010dcb0..7b524c14089d 100644 --- a/x/mint/README.md +++ b/x/mint/README.md @@ -83,7 +83,7 @@ inflation calculation logic is needed, this can be achieved by defining and passing a function that matches `InflationCalculationFn`'s signature. ```go -type InflationCalculationFn func(ctx sdk.Context, minter Minter, params Params, bondedRatio math.LegacyDec) math.LegacyDec +type InflationCalculationFn func(ctx sdk.Context, minter Minter, params Params, bondedRatio math.Dec) math.Dec ``` #### NextInflationRate @@ -95,7 +95,7 @@ possible is defined to be 13% per year, however, the annual inflation is capped as between 7% and 20%. ```go -NextInflationRate(params Params, bondedRatio math.LegacyDec) (inflation math.LegacyDec) { +NextInflationRate(params Params, bondedRatio math.Dec) (inflation math.Dec) { inflationRateChangePerYear = (1 - bondedRatio/params.GoalBonded) * params.InflationRateChange inflationRateChange = inflationRateChangePerYear/blocksPerYr @@ -118,7 +118,7 @@ Calculate the annual provisions based on current total supply and inflation rate. This parameter is calculated once per block. ```go -NextAnnualProvisions(params Params, totalSupply math.LegacyDec) (provisions math.LegacyDec) { +NextAnnualProvisions(params Params, totalSupply math.Dec) (provisions math.Dec) { return Inflation * totalSupply ``` From 930db0d67830d2ac92615db7c05908e5c8ae146e Mon Sep 17 00:00:00 2001 From: samricotta Date: Mon, 22 Apr 2024 12:36:16 +0200 Subject: [PATCH 05/22] Revert "Update to docs" This reverts commit 115ecc4e2768840a36bec6ac155a6f8c97276a01. --- docs/architecture/adr-069-gov-improvements.md | 2 +- x/distribution/README.md | 2 +- x/mint/README.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/architecture/adr-069-gov-improvements.md b/docs/architecture/adr-069-gov-improvements.md index 7b156f21c36c..af5b12645205 100644 --- a/docs/architecture/adr-069-gov-improvements.md +++ b/docs/architecture/adr-069-gov-improvements.md @@ -172,7 +172,7 @@ type CalculateVoteResultsAndVotingPowerFn func( keeper Keeper, proposalID uint64, validators map[string]v1.ValidatorGovInfo, -) (totalVoterPower math.Dec, results map[v1.VoteOption]math.Dec, err error) +) (totalVoterPower math.LegacyDec, results map[v1.VoteOption]math.LegacyDec, err error) ``` ## Consequences diff --git a/x/distribution/README.md b/x/distribution/README.md index 002e15ed3c78..03e1e5baff21 100644 --- a/x/distribution/README.md +++ b/x/distribution/README.md @@ -139,7 +139,7 @@ Once those rewards are big enough, they are sent as `sdk.Coins` to the community type DecCoins []DecCoin type DecCoin struct { - Amount math.Dec + Amount math.LegacyDec Denom string } ``` diff --git a/x/mint/README.md b/x/mint/README.md index 7b524c14089d..80198010dcb0 100644 --- a/x/mint/README.md +++ b/x/mint/README.md @@ -83,7 +83,7 @@ inflation calculation logic is needed, this can be achieved by defining and passing a function that matches `InflationCalculationFn`'s signature. ```go -type InflationCalculationFn func(ctx sdk.Context, minter Minter, params Params, bondedRatio math.Dec) math.Dec +type InflationCalculationFn func(ctx sdk.Context, minter Minter, params Params, bondedRatio math.LegacyDec) math.LegacyDec ``` #### NextInflationRate @@ -95,7 +95,7 @@ possible is defined to be 13% per year, however, the annual inflation is capped as between 7% and 20%. ```go -NextInflationRate(params Params, bondedRatio math.Dec) (inflation math.Dec) { +NextInflationRate(params Params, bondedRatio math.LegacyDec) (inflation math.LegacyDec) { inflationRateChangePerYear = (1 - bondedRatio/params.GoalBonded) * params.InflationRateChange inflationRateChange = inflationRateChangePerYear/blocksPerYr @@ -118,7 +118,7 @@ Calculate the annual provisions based on current total supply and inflation rate. This parameter is calculated once per block. ```go -NextAnnualProvisions(params Params, totalSupply math.Dec) (provisions math.Dec) { +NextAnnualProvisions(params Params, totalSupply math.LegacyDec) (provisions math.LegacyDec) { return Inflation * totalSupply ``` From f75c9827b2a5d75e231befc4eb8160be3130ecf3 Mon Sep 17 00:00:00 2001 From: samricotta Date: Wed, 24 Apr 2024 14:25:38 +0300 Subject: [PATCH 06/22] Create 18-decimal-handling.md --- .../building-modules/18-decimal-handling.md | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/build/building-modules/18-decimal-handling.md diff --git a/docs/build/building-modules/18-decimal-handling.md b/docs/build/building-modules/18-decimal-handling.md new file mode 100644 index 000000000000..cf97e2e30c1b --- /dev/null +++ b/docs/build/building-modules/18-decimal-handling.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 1 +--- +# Decimal Handling in Cosmos SDK + +:::note +As part of ongoing improvements to the Cosmos SDK, we have updated our decimal handling from `LegacyDec` to `Dec`. This update is crucial for modules that perform mathematical computations, ensuring higher precision and better performance. +::: + +## Introduction + +In the Cosmos SDK we have 2 types of decimals LegacyDec and Dec. `LegacyDec` is the old decimal type that was used, which is still available. `Dec` is the new decimal type and is more performant than `LegacyDec`. + +## Why the Change? + +* **Enhanced Precision**: `Dec` uses the [apd](https://github.com/cockroachdb/apd) library for arbitrary precision decimals, suitable for accurate financial calculations. +* **Immutable Operations**: `Dec` operations are safer for concurrent use as they do not mutate the original values. +* **Better Performance**: `Dec` operations are faster and more efficient than `LegacyDec`. + +Benchmarking results below between `LegacyDec` and `Dec`: + +``` +BenchmarkCompareLegacyDecAndNewDec/LegacyDec-10 8621032 143.8 ns/op 144 B/op 3 allocs/op +BenchmarkCompareLegacyDecAndNewDec/NewDec-10 5206173 238.7 ns/op 176 B/op 7 allocs/op +BenchmarkCompareLegacyDecAndNewDecQuoInteger/LegacyDec-10 5767692 205.1 ns/op 232 B/op 6 allocs/op +BenchmarkCompareLegacyDecAndNewDecQuoInteger/NewDec-10 23172602 51.75 ns/op 16 B/op 2 allocs/op +BenchmarkCompareLegacyAddAndDecAdd/LegacyDec-10 21157941 56.33 ns/op 80 B/op 2 allocs/op +BenchmarkCompareLegacyAddAndDecAdd/NewDec-10 24133659 48.92 ns/op 48 B/op 1 allocs/op +BenchmarkCompareLegacySubAndDecMul/LegacyDec-10 14256832 87.47 ns/op 80 B/op 2 allocs/op +BenchmarkCompareLegacySubAndDecMul/NewDec-10 18273994 65.68 ns/op 48 B/op 1 allocs/op +BenchmarkCompareLegacySubAndDecSub/LegacyDec-10 19988325 64.46 ns/op 80 B/op 2 allocs/op +BenchmarkCompareLegacySubAndDecSub/NewDec-10 27430347 42.45 ns/op 8 B/op 1 allocs/op +``` + +## Updating Your Modules + +Modules using `LegacyDec` should transition to `Dec` to maintain compatibility with the latest SDK updates. This involves: + +1. Updating type declarations from `LegacyDec` to `Dec`. +2. Modifying arithmetic operations to handle the new method signatures and potential errors. + +# Example Update + +Transitioning an addition operation from `LegacyDec` to `Dec`: + +**Before:** + +```go +result := legacyDec1.Add(legacyDec2) +``` + +**After:** + +```go +result, err := dec1.Add(dec2) +if err != nil { + log.Fatalf("Error during addition: %v", err) +} +``` + +This can be done for all arithmetic operations, including subtraction, multiplication, division, and more. From 9f3fc5770f8d79fc9bca08d48edc7cf4ba8424fd Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 25 Apr 2024 15:20:28 +0300 Subject: [PATCH 07/22] wip hash migration --- testutil/dec.go | 37 ++++++++++++++++++++++++++++++ testutil/math_utils_test.go | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 testutil/dec.go create mode 100644 testutil/math_utils_test.go diff --git a/testutil/dec.go b/testutil/dec.go new file mode 100644 index 000000000000..6f0c937e2436 --- /dev/null +++ b/testutil/dec.go @@ -0,0 +1,37 @@ +package testutil + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + + storetypes "cosmossdk.io/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func DiffDecimalsMigration( + ctx sdk.Context, + storeKey *storetypes.KVStoreKey, + iterations int, + migrateDec func(int64), + targetHash string, +) error { + for i := int64(0); i < int64(iterations); i++ { + migrateDec(i) + } + + h := sha256.New() + it := ctx.KVStore(storeKey).Iterator(nil, nil) + defer it.Close() + for ; it.Valid(); it.Next() { + h.Write(it.Key()) + h.Write(it.Value()) + } + + hash := h.Sum(nil) + if hex.EncodeToString(hash) != targetHash { + return fmt.Errorf("hashes don't match: %s != %s", hex.EncodeToString(hash), targetHash) + } + + return nil +} diff --git a/testutil/math_utils_test.go b/testutil/math_utils_test.go new file mode 100644 index 000000000000..44b71cb2bbe6 --- /dev/null +++ b/testutil/math_utils_test.go @@ -0,0 +1,45 @@ +package testutil_test + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "testing" + + math "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestDiffDecimalsMigration(t *testing.T) { + key := storetypes.NewKVStoreKey("test") + ctx := testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) + for i := int64(0); i < 5; i++ { + legacyDec := math.LegacyNewDec(i) + dec := math.NewDecFromInt64(i) + ctx.KVStore(key).Set([]byte(fmt.Sprintf("legacy_%d", i)), []byte(legacyDec.String())) + ctx.KVStore(key).Set([]byte(fmt.Sprintf("new_%d", i)), []byte(dec.String())) + } + + hashLegacy := computeHash(ctx, key, "legacy_") + hashNew := computeHash(ctx, key, "new_") + require.Equal(t, hashLegacy, hashNew, "Hashes do not match") +} + +func computeHash(ctx sdk.Context, key storetypes.StoreKey, prefix string) string { + h := sha256.New() + start, end := prefixRange(prefix) + it := ctx.KVStore(key).Iterator(start, end) + defer it.Close() + for ; it.Valid(); it.Next() { + h.Write(it.Key()) + h.Write(it.Value()) + } + return hex.EncodeToString(h.Sum(nil)) +} + +func prefixRange(prefix string) (start, end []byte) { + return []byte(prefix), append([]byte(prefix), 0xFF) +} From 872c56fd8daf353b8d9df9c18c402903da77754f Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 25 Apr 2024 15:45:21 +0300 Subject: [PATCH 08/22] Revert "wip hash migration" This reverts commit 9f3fc5770f8d79fc9bca08d48edc7cf4ba8424fd. --- testutil/dec.go | 37 ------------------------------ testutil/math_utils_test.go | 45 ------------------------------------- 2 files changed, 82 deletions(-) delete mode 100644 testutil/dec.go delete mode 100644 testutil/math_utils_test.go diff --git a/testutil/dec.go b/testutil/dec.go deleted file mode 100644 index 6f0c937e2436..000000000000 --- a/testutil/dec.go +++ /dev/null @@ -1,37 +0,0 @@ -package testutil - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - - storetypes "cosmossdk.io/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -func DiffDecimalsMigration( - ctx sdk.Context, - storeKey *storetypes.KVStoreKey, - iterations int, - migrateDec func(int64), - targetHash string, -) error { - for i := int64(0); i < int64(iterations); i++ { - migrateDec(i) - } - - h := sha256.New() - it := ctx.KVStore(storeKey).Iterator(nil, nil) - defer it.Close() - for ; it.Valid(); it.Next() { - h.Write(it.Key()) - h.Write(it.Value()) - } - - hash := h.Sum(nil) - if hex.EncodeToString(hash) != targetHash { - return fmt.Errorf("hashes don't match: %s != %s", hex.EncodeToString(hash), targetHash) - } - - return nil -} diff --git a/testutil/math_utils_test.go b/testutil/math_utils_test.go deleted file mode 100644 index 44b71cb2bbe6..000000000000 --- a/testutil/math_utils_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package testutil_test - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "testing" - - math "cosmossdk.io/math" - storetypes "cosmossdk.io/store/types" - "github.com/cosmos/cosmos-sdk/testutil" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" -) - -func TestDiffDecimalsMigration(t *testing.T) { - key := storetypes.NewKVStoreKey("test") - ctx := testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) - for i := int64(0); i < 5; i++ { - legacyDec := math.LegacyNewDec(i) - dec := math.NewDecFromInt64(i) - ctx.KVStore(key).Set([]byte(fmt.Sprintf("legacy_%d", i)), []byte(legacyDec.String())) - ctx.KVStore(key).Set([]byte(fmt.Sprintf("new_%d", i)), []byte(dec.String())) - } - - hashLegacy := computeHash(ctx, key, "legacy_") - hashNew := computeHash(ctx, key, "new_") - require.Equal(t, hashLegacy, hashNew, "Hashes do not match") -} - -func computeHash(ctx sdk.Context, key storetypes.StoreKey, prefix string) string { - h := sha256.New() - start, end := prefixRange(prefix) - it := ctx.KVStore(key).Iterator(start, end) - defer it.Close() - for ; it.Valid(); it.Next() { - h.Write(it.Key()) - h.Write(it.Value()) - } - return hex.EncodeToString(h.Sum(nil)) -} - -func prefixRange(prefix string) (start, end []byte) { - return []byte(prefix), append([]byte(prefix), 0xFF) -} From 9699b0deaad8cb529a14a99b060cac20fd10d5e4 Mon Sep 17 00:00:00 2001 From: samricotta Date: Fri, 26 Apr 2024 13:38:05 +0300 Subject: [PATCH 09/22] wip test migration hash --- go.mod | 2 + go.sum | 2 + math/dec_bench_test.go | 29 +++++----- math/go.mod | 4 +- testutil/collections_test.go | 3 +- testutil/math_test.go | 101 +++++++++++++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 testutil/math_test.go diff --git a/go.mod b/go.mod index 81cc2c5006dd..6f7ef577fde2 100644 --- a/go.mod +++ b/go.mod @@ -80,6 +80,7 @@ require ( github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/pebble v1.1.0 // indirect github.com/cockroachdb/redact v1.1.5 // indirect @@ -183,6 +184,7 @@ replace ( cosmossdk.io/api => ./api cosmossdk.io/core => ./core cosmossdk.io/depinject => ./depinject + cosmossdk.io/math => ./math cosmossdk.io/x/accounts => ./x/accounts cosmossdk.io/x/auth => ./x/auth cosmossdk.io/x/bank => ./x/bank diff --git a/go.sum b/go.sum index 60519050aa66..44376fcff119 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= +github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= diff --git a/math/dec_bench_test.go b/math/dec_bench_test.go index 00ee00a8477d..3bd0f0053988 100644 --- a/math/dec_bench_test.go +++ b/math/dec_bench_test.go @@ -2,6 +2,8 @@ package math import ( "testing" + + "github.com/stretchr/testify/require" ) func BenchmarkCompareLegacyDecAndNewDec(b *testing.B) { @@ -78,21 +80,16 @@ func BenchmarkCompareLegacySubAndDecMul(b *testing.B) { }) } -func BenchmarkCompareLegacySubAndDecSub(b *testing.B) { - legacyB1 := LegacyNewDec(100) - legacyB2 := LegacyNewDec(5) - newB1 := NewDecFromInt64(100) - newB2 := NewDecFromInt64(5) +func TestMigration(t *testing.T) { + legacyDec, _ := LegacyNewDecFromStr("123.456") + newDec, err := MigrateLegacyDecToDec(legacyDec) + require.NoError(t, err) - b.Run("LegacyDec", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = legacyB1.Sub(legacyB2) - } - }) + expectedDec, _ := NewDecFromString("123.456") + require.True(t, newDec.Equal(expectedDec)) +} - b.Run("NewDec", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _, _ = newB1.Sub(newB2) - } - }) -} \ No newline at end of file +func MigrateLegacyDecToDec(legacyDec LegacyDec) (Dec, error) { + str := legacyDec.String() + return NewDecFromString(str) +} diff --git a/math/go.mod b/math/go.mod index 0f897d363421..4130dd995dd5 100644 --- a/math/go.mod +++ b/math/go.mod @@ -1,6 +1,8 @@ module cosmossdk.io/math -go 1.20 +go 1.21 + +toolchain go1.22.2 require ( github.com/stretchr/testify v1.9.0 diff --git a/testutil/collections_test.go b/testutil/collections_test.go index a35533539efc..9d1db1108143 100644 --- a/testutil/collections_test.go +++ b/testutil/collections_test.go @@ -14,7 +14,6 @@ func TestDiffCollectionsMigration(t *testing.T) { key := storetypes.NewKVStoreKey("test") ctx := testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) - // First try with some invalid hash err := testutil.DiffCollectionsMigration( ctx, key, @@ -26,7 +25,7 @@ func TestDiffCollectionsMigration(t *testing.T) { ) require.Error(t, err) - // Now reset and try with the correct hash + ctx = testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) err = testutil.DiffCollectionsMigration( ctx, diff --git a/testutil/math_test.go b/testutil/math_test.go new file mode 100644 index 000000000000..cfa1413fbd15 --- /dev/null +++ b/testutil/math_test.go @@ -0,0 +1,101 @@ +package testutil_test + +import ( + "encoding/json" + "fmt" + "testing" + + math "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +type testLegDecCoin struct { + Denom string + Amount int64 + Fee sdk.DecCoin +} + +type testDecCoin struct { + Denom string + Amount int64 + Fee math.Dec +} + +func TestDiffDecimalsMigrationWithLDec(t *testing.T) { + key := storetypes.NewKVStoreKey("test") + ctx := testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) + + err := testutil.DiffCollectionsMigration( + ctx, + key, + 100, + func(i int64) { + legacyDec := testLegDecCoin{ + Denom: "test", + Amount: i, + Fee: sdk.NewDecCoinFromDec("test", math.LegacyNewDec(100)), + } + + feeBytes, err := json.Marshal(legacyDec.Fee) + if err != nil { + t.Fatal(err) + } + + ctx.KVStore(key).Set([]byte(fmt.Sprintf("%d", i)), feeBytes) + }, + "somerandomhashtostartwith", + ) + require.Error(t, err) + + ctx = testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) + + err = testutil.DiffCollectionsMigration( + ctx, + key, + 100, + func(i int64) { + legacyDec := testLegDecCoin{ + Denom: "test", + Amount: i, + Fee: sdk.NewDecCoinFromDec("test", math.LegacyNewDec(100)), + } + + feeBytes, err := json.Marshal(legacyDec.Fee) + if err != nil { + t.Fatal(err) + } + + ctx.KVStore(key).Set([]byte(fmt.Sprintf("%d", i)), feeBytes) + }, + "4b782f32948a596f8507f09817eec2307ce2ffee1aba5a548004cccb062ccdbd", + ) + require.NoError(t, err) + + + ctx = testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) + + err = testutil.DiffCollectionsMigration( + ctx, + key, + 100, + func(i int64) { + Dec := testDecCoin{ + Denom: "test", + Amount: i, + Fee: math.NewDecFromInt64(100), + } + + feeBytes, err := json.Marshal(Dec.Fee) + if err != nil { + t.Fatal(err) + } + + ctx.KVStore(key).Set([]byte(fmt.Sprintf("%d", i)), feeBytes) + }, + "4b782f32948a596f8507f09817eec2307ce2ffee1aba5a548004cccb062ccdbd", + ) + require.NoError(t, err) +} From 4695add469a2f663562bdf3ee46d5ac6dcd59594 Mon Sep 17 00:00:00 2001 From: samricotta Date: Fri, 26 Apr 2024 13:49:45 +0300 Subject: [PATCH 10/22] pt 2 --- testutil/math_test.go | 4 ++-- types/dec_coin.go | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/testutil/math_test.go b/testutil/math_test.go index cfa1413fbd15..6fbba1b6a476 100644 --- a/testutil/math_test.go +++ b/testutil/math_test.go @@ -82,10 +82,10 @@ func TestDiffDecimalsMigrationWithLDec(t *testing.T) { key, 100, func(i int64) { - Dec := testDecCoin{ + Dec := testLegDecCoin{ Denom: "test", Amount: i, - Fee: math.NewDecFromInt64(100), + Fee: sdk.NewDecNonLegacyCoinFromDec("test", math.NewDecFromInt64(100)), } feeBytes, err := json.Marshal(Dec.Fee) diff --git a/types/dec_coin.go b/types/dec_coin.go index 37bcd5b633a9..3c8b6a441c20 100644 --- a/types/dec_coin.go +++ b/types/dec_coin.go @@ -37,6 +37,20 @@ func NewDecCoinFromDec(denom string, amount math.LegacyDec) DecCoin { } } +// NewDecCoinFromDec creates a new DecCoin instance from a Dec. +func NewDecNonLegacyCoinFromDec(denom string, amount math.Dec) DecCoin { + mustValidateDenom(denom) + + if amount.IsNegative() { + panic(fmt.Sprintf("negative decimal coin amount: %v\n", amount)) + } + + return DecCoin{ + Denom: denom, + Amount: amount, + } +} + // NewDecCoinFromCoin creates a new DecCoin from a Coin. func NewDecCoinFromCoin(coin Coin) DecCoin { if err := coin.Validate(); err != nil { From d1d7d2d966a201bb1f8459c3b8ad8dce2440b477 Mon Sep 17 00:00:00 2001 From: samricotta Date: Tue, 30 Apr 2024 23:51:05 +0300 Subject: [PATCH 11/22] Update 18-decimal-handling.md --- .../building-modules/18-decimal-handling.md | 121 +++++++++++++----- 1 file changed, 92 insertions(+), 29 deletions(-) diff --git a/docs/build/building-modules/18-decimal-handling.md b/docs/build/building-modules/18-decimal-handling.md index cf97e2e30c1b..dea5533770c0 100644 --- a/docs/build/building-modules/18-decimal-handling.md +++ b/docs/build/building-modules/18-decimal-handling.md @@ -3,59 +3,122 @@ sidebar_position: 1 --- # Decimal Handling in Cosmos SDK -:::note -As part of ongoing improvements to the Cosmos SDK, we have updated our decimal handling from `LegacyDec` to `Dec`. This update is crucial for modules that perform mathematical computations, ensuring higher precision and better performance. -::: - ## Introduction -In the Cosmos SDK we have 2 types of decimals LegacyDec and Dec. `LegacyDec` is the old decimal type that was used, which is still available. `Dec` is the new decimal type and is more performant than `LegacyDec`. +In the Cosmos SDK we have 2 types of decimals `LegacyDec` and `Dec`. `LegacyDec` is the old decimal type that was used, which is still available to be used and `Dec` is the new decimal type and is more performant than `LegacyDec`. These are state-breaking changes and will require an upgrade but it is recommended to use `Dec` for new modules. ## Why the Change? * **Enhanced Precision**: `Dec` uses the [apd](https://github.com/cockroachdb/apd) library for arbitrary precision decimals, suitable for accurate financial calculations. * **Immutable Operations**: `Dec` operations are safer for concurrent use as they do not mutate the original values. -* **Better Performance**: `Dec` operations are faster and more efficient than `LegacyDec`. +* **Better Performance**: `Dec` operations are faster and more efficient than `LegacyDec`.` + +## Using `Dec` in Modules that havent used `LegacyDec` + +If you are creating a new module or updating an existing module that has not used `LegacyDec`, you can directly use `Dec` without any changes. + +As an example we will use `DecCoin` which is a common type used in the Cosmos SDK. + -Benchmarking results below between `LegacyDec` and `Dec`: +```protobuf +message DecCoin { + option (gogoproto.equal) = true; + string denom = 1; + string amount = 2 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "cosmossdk.io/math.Dec", + (gogoproto.nullable) = false + ]; +} ``` -BenchmarkCompareLegacyDecAndNewDec/LegacyDec-10 8621032 143.8 ns/op 144 B/op 3 allocs/op -BenchmarkCompareLegacyDecAndNewDec/NewDec-10 5206173 238.7 ns/op 176 B/op 7 allocs/op -BenchmarkCompareLegacyDecAndNewDecQuoInteger/LegacyDec-10 5767692 205.1 ns/op 232 B/op 6 allocs/op -BenchmarkCompareLegacyDecAndNewDecQuoInteger/NewDec-10 23172602 51.75 ns/op 16 B/op 2 allocs/op -BenchmarkCompareLegacyAddAndDecAdd/LegacyDec-10 21157941 56.33 ns/op 80 B/op 2 allocs/op -BenchmarkCompareLegacyAddAndDecAdd/NewDec-10 24133659 48.92 ns/op 48 B/op 1 allocs/op -BenchmarkCompareLegacySubAndDecMul/LegacyDec-10 14256832 87.47 ns/op 80 B/op 2 allocs/op -BenchmarkCompareLegacySubAndDecMul/NewDec-10 18273994 65.68 ns/op 48 B/op 1 allocs/op -BenchmarkCompareLegacySubAndDecSub/LegacyDec-10 19988325 64.46 ns/op 80 B/op 2 allocs/op -BenchmarkCompareLegacySubAndDecSub/NewDec-10 27430347 42.45 ns/op 8 B/op 1 allocs/op + +How you can implement `Dec` in your module: + +```go +import ( + "cosmossdk.io/math" +) + +example := math.NewDecFromInt64(100) ``` -## Updating Your Modules +# Modules migrating from `LegacyDec` to `Dec` + +When migrating from `LegacyDec` to `Dec`, you need to update your module to use the new decimal type. **These types are state breaking changes and require a migration.** + +## Precision Handling + +The reason for the state breaking change is the difference in precision handling between the two decimal types: + +* **LegacyDec**: Fixed precision of 18 decimal places. +* **Dec**: Flexible precision up to 34 decimal places using the apd library. + +## Byte Representation Changes Example + +The change in precision handling directly impacts the byte representation of decimal values: + +**Legacy Dec Byte Representation:** +`2333435363738393030303030303030303030303030303030303030` -Modules using `LegacyDec` should transition to `Dec` to maintain compatibility with the latest SDK updates. This involves: +This example includes the value 123456789 followed by 18 zeros to maintain the fixed precision. -1. Updating type declarations from `LegacyDec` to `Dec`. -2. Modifying arithmetic operations to handle the new method signatures and potential errors. +**New Dec Byte Representation:** +`0a03617364121031323334353637383900000000000000` -# Example Update +This example shows the value 123456789 without additional padding, reflecting the flexible precision handling of the new Dec type. -Transitioning an addition operation from `LegacyDec` to `Dec`: +## Impact of Precision Change + +The increase in precision from 18 to 34 decimal places allows for more detailed decimal values but requires data migration. This change in how data is formatted and stored is a key aspect of why the transition is considered state-breaking. + +## Example of State-Breaking Change + +The protobuf definitions for DecCoin illustrate the change in the custom type for the amount field. **Before:** -```go -result := legacyDec1.Add(legacyDec2) +```protobuf +message DecCoin { + option (gogoproto.equal) = true; + + string denom = 1; + string amount = 2 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "cosmossdk.io/math.LegacyDec", + (gogoproto.nullable) = false + ]; +} ``` **After:** +```protobuf +message DecCoin { + option (gogoproto.equal) = true; + + string denom = 1; + string amount = 2 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "cosmossdk.io/math.Dec", + (gogoproto.nullable) = false + ]; +} +``` + +## Converting `LegacyDec` to `Dec` without storing the data + +If you would like to convert a `LegacyDec` to a `Dec` without a state migration changing how the data is handled internally within the application logic and not how it's stored or represented. You can use the following methods. + +```go +func LegacyDecToDec(ld LegacyDec) (Dec, error) { + return NewDecFromString(ld.String()) +} +``` + ```go -result, err := dec1.Add(dec2) -if err != nil { - log.Fatalf("Error during addition: %v", err) +func DecToLegacyDec(ld Dec) (LegacyDec, error) { + return LegacyDecFromString(ld.String()) } ``` -This can be done for all arithmetic operations, including subtraction, multiplication, division, and more. From d42a903a8fded64fa4ad5a023f409c4118711236 Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 2 May 2024 11:49:14 +0300 Subject: [PATCH 12/22] update tests and convert func --- math/dec_legacy.go | 4 ++++ math/dec_legacy_test.go | 10 ++++++++++ math/dec_test.go | 10 ++++++++++ 3 files changed, 24 insertions(+) diff --git a/math/dec_legacy.go b/math/dec_legacy.go index 0ca1cfcb8c22..cbad1038a2d5 100644 --- a/math/dec_legacy.go +++ b/math/dec_legacy.go @@ -967,3 +967,7 @@ func FormatDec(v string) (string, error) { return intPart + "." + decPart, nil } + +func LegacyDecToDec(ld LegacyDec) (Dec, error) { + return NewDecFromString(ld.String()) +} \ No newline at end of file diff --git a/math/dec_legacy_test.go b/math/dec_legacy_test.go index 96d7231a9413..7883eadbadc3 100644 --- a/math/dec_legacy_test.go +++ b/math/dec_legacy_test.go @@ -782,3 +782,13 @@ func (s *decimalTestSuite) TestConvertToBigIntMutativeForLegacyDec() { s.Require().NotEqual(big.NewInt(50), i.BigIntMut()) s.Require().NotEqual(big.NewInt(50), i.BigInt()) } + +func TestLegacyDecToDec(t *testing.T) { + legacyDec, _ := math.LegacyNewDecFromStr("123.000000000000000000") + + dec, err := math.LegacyDecToDec(legacyDec) + require.NoError(t, err) + + expected, _ := math.NewDecFromString("123.000000000000000000") + require.True(t, dec.Equal(expected)) +} diff --git a/math/dec_test.go b/math/dec_test.go index b921435a09c7..94c64cc99d10 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -711,3 +711,13 @@ func TestInfDecString(t *testing.T) { require.Error(t, err) require.ErrorIs(t, err, ErrInfiniteString) } + +func TestDecToLegacyDec(t *testing.T) { + dec := NewDecFromInt64(123) + + legacyDec, err := DecToLegacyDec(dec) + require.NoError(t, err) + + expected, _ := LegacyNewDecFromStr("123.000000000000000000") + require.True(t, legacyDec.Equal(expected)) +} \ No newline at end of file From af8acc2b1f30fb68a963f1e77e19b094c96a9542 Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 2 May 2024 12:41:59 +0300 Subject: [PATCH 13/22] Update 18-decimal-handling.md --- docs/build/building-modules/18-decimal-handling.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/build/building-modules/18-decimal-handling.md b/docs/build/building-modules/18-decimal-handling.md index dea5533770c0..c3b6a2eb3e86 100644 --- a/docs/build/building-modules/18-decimal-handling.md +++ b/docs/build/building-modules/18-decimal-handling.md @@ -5,7 +5,12 @@ sidebar_position: 1 ## Introduction -In the Cosmos SDK we have 2 types of decimals `LegacyDec` and `Dec`. `LegacyDec` is the old decimal type that was used, which is still available to be used and `Dec` is the new decimal type and is more performant than `LegacyDec`. These are state-breaking changes and will require an upgrade but it is recommended to use `Dec` for new modules. +In the Cosmos SDK, there are two types of decimals: `LegacyDec` and `Dec`. `LegacyDec` is the older decimal type that is still available for use, while `Dec` is the newer, more performant decimal type. The implementation of `Dec` is adapted from Regen Network's `regen-ledger`, specifically from [this module](https://github.com/regen-network/regen-ledger/tree/main/types/math). Migrating from `LegacyDec` to `Dec` involves state-breaking changes, specifically: + +* **Data Format**: The internal representation of decimals changes, affecting how data is stored and processed. +* **Precision Handling**: `Dec` supports flexible precision up to 34 decimal places, unlike `LegacyDec` which has a fixed precision of 18 decimal places. + +These changes require a state migration to update existing decimal values to the new format. It is recommended to use `Dec` for new modules to leverage its enhanced performance and flexibility. ## Why the Change? @@ -13,7 +18,7 @@ In the Cosmos SDK we have 2 types of decimals `LegacyDec` and `Dec`. `LegacyDec` * **Immutable Operations**: `Dec` operations are safer for concurrent use as they do not mutate the original values. * **Better Performance**: `Dec` operations are faster and more efficient than `LegacyDec`.` -## Using `Dec` in Modules that havent used `LegacyDec` +## Using `Dec` in Modules that haven't used `LegacyDec` If you are creating a new module or updating an existing module that has not used `LegacyDec`, you can directly use `Dec` without any changes. @@ -43,7 +48,7 @@ import ( example := math.NewDecFromInt64(100) ``` -# Modules migrating from `LegacyDec` to `Dec` +## Modules migrating from `LegacyDec` to `Dec` When migrating from `LegacyDec` to `Dec`, you need to update your module to use the new decimal type. **These types are state breaking changes and require a migration.** From 0701e66e7b0d6affa39edfca17ba23a7f2ccb938 Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 2 May 2024 13:25:45 +0300 Subject: [PATCH 14/22] remove migrations changes --- .../building-modules/18-decimal-handling.md | 4 +- testutil/collections_test.go | 3 +- testutil/math_test.go | 101 ------------------ 3 files changed, 4 insertions(+), 104 deletions(-) delete mode 100644 testutil/math_test.go diff --git a/docs/build/building-modules/18-decimal-handling.md b/docs/build/building-modules/18-decimal-handling.md index c3b6a2eb3e86..a70d731bf1b5 100644 --- a/docs/build/building-modules/18-decimal-handling.md +++ b/docs/build/building-modules/18-decimal-handling.md @@ -15,14 +15,14 @@ These changes require a state migration to update existing decimal values to the ## Why the Change? * **Enhanced Precision**: `Dec` uses the [apd](https://github.com/cockroachdb/apd) library for arbitrary precision decimals, suitable for accurate financial calculations. -* **Immutable Operations**: `Dec` operations are safer for concurrent use as they do not mutate the original values. +* **Immutable Operations**: `Dec` operations are safer for concurrent use, as they do not mutate the original values. * **Better Performance**: `Dec` operations are faster and more efficient than `LegacyDec`.` ## Using `Dec` in Modules that haven't used `LegacyDec` If you are creating a new module or updating an existing module that has not used `LegacyDec`, you can directly use `Dec` without any changes. -As an example we will use `DecCoin` which is a common type used in the Cosmos SDK. +As an example, we will use `DecCoin` which is a common type used in the Cosmos SDK. ```protobuf diff --git a/testutil/collections_test.go b/testutil/collections_test.go index 9d1db1108143..a35533539efc 100644 --- a/testutil/collections_test.go +++ b/testutil/collections_test.go @@ -14,6 +14,7 @@ func TestDiffCollectionsMigration(t *testing.T) { key := storetypes.NewKVStoreKey("test") ctx := testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) + // First try with some invalid hash err := testutil.DiffCollectionsMigration( ctx, key, @@ -25,7 +26,7 @@ func TestDiffCollectionsMigration(t *testing.T) { ) require.Error(t, err) - + // Now reset and try with the correct hash ctx = testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) err = testutil.DiffCollectionsMigration( ctx, diff --git a/testutil/math_test.go b/testutil/math_test.go deleted file mode 100644 index 6fbba1b6a476..000000000000 --- a/testutil/math_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package testutil_test - -import ( - "encoding/json" - "fmt" - "testing" - - math "cosmossdk.io/math" - storetypes "cosmossdk.io/store/types" - "github.com/cosmos/cosmos-sdk/testutil" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" -) - -type testLegDecCoin struct { - Denom string - Amount int64 - Fee sdk.DecCoin -} - -type testDecCoin struct { - Denom string - Amount int64 - Fee math.Dec -} - -func TestDiffDecimalsMigrationWithLDec(t *testing.T) { - key := storetypes.NewKVStoreKey("test") - ctx := testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) - - err := testutil.DiffCollectionsMigration( - ctx, - key, - 100, - func(i int64) { - legacyDec := testLegDecCoin{ - Denom: "test", - Amount: i, - Fee: sdk.NewDecCoinFromDec("test", math.LegacyNewDec(100)), - } - - feeBytes, err := json.Marshal(legacyDec.Fee) - if err != nil { - t.Fatal(err) - } - - ctx.KVStore(key).Set([]byte(fmt.Sprintf("%d", i)), feeBytes) - }, - "somerandomhashtostartwith", - ) - require.Error(t, err) - - ctx = testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) - - err = testutil.DiffCollectionsMigration( - ctx, - key, - 100, - func(i int64) { - legacyDec := testLegDecCoin{ - Denom: "test", - Amount: i, - Fee: sdk.NewDecCoinFromDec("test", math.LegacyNewDec(100)), - } - - feeBytes, err := json.Marshal(legacyDec.Fee) - if err != nil { - t.Fatal(err) - } - - ctx.KVStore(key).Set([]byte(fmt.Sprintf("%d", i)), feeBytes) - }, - "4b782f32948a596f8507f09817eec2307ce2ffee1aba5a548004cccb062ccdbd", - ) - require.NoError(t, err) - - - ctx = testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) - - err = testutil.DiffCollectionsMigration( - ctx, - key, - 100, - func(i int64) { - Dec := testLegDecCoin{ - Denom: "test", - Amount: i, - Fee: sdk.NewDecNonLegacyCoinFromDec("test", math.NewDecFromInt64(100)), - } - - feeBytes, err := json.Marshal(Dec.Fee) - if err != nil { - t.Fatal(err) - } - - ctx.KVStore(key).Set([]byte(fmt.Sprintf("%d", i)), feeBytes) - }, - "4b782f32948a596f8507f09817eec2307ce2ffee1aba5a548004cccb062ccdbd", - ) - require.NoError(t, err) -} From e5e0f6563682ad6d72c5f0f8f8154392aff701d9 Mon Sep 17 00:00:00 2001 From: samricotta Date: Mon, 6 May 2024 11:18:05 +0300 Subject: [PATCH 15/22] Update doc.go Co-Authored-By: Aaron Craelius --- math/doc.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/math/doc.go b/math/doc.go index 17664a9531e0..c3ea3346ddf6 100644 --- a/math/doc.go +++ b/math/doc.go @@ -2,5 +2,11 @@ Package math implements custom Cosmos SDK math types used for arithmetic operations. Signed and unsigned integer types utilize Golang's standard library big integers types, having a maximum bit length of 256 bits. + +This code is based on the implementation found in the Regen Ledger project: +https://github.com/regen-network/regen-ledger/tree/main/types/math + +Co-authors and contributors include: +Regen Network */ package math From 3320c005f616640e67d1dae70ebfcb9b182cd6fb Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Mon, 6 May 2024 11:45:30 +0200 Subject: [PATCH 16/22] Refactor constructors --- math/dec.go | 88 +++-- math/dec_rapid_test.go | 598 +++++++++++++++++++++++++++++++++ math/dec_test.go | 725 +++++++---------------------------------- 3 files changed, 764 insertions(+), 647 deletions(-) create mode 100644 math/dec_rapid_test.go diff --git a/math/dec.go b/math/dec.go index 2736bf67a60f..f3bd7e175c8c 100644 --- a/math/dec.go +++ b/math/dec.go @@ -1,7 +1,6 @@ package math import ( - "fmt" "math/big" "cosmossdk.io/errors" @@ -45,65 +44,64 @@ var dec128Context = apd.Context{ Traps: apd.DefaultTraps, } -func NewDecFromString(s string) (Dec, error) { - if s == "" { - s = "0" - } - d, _, err := apd.NewFromString(s) - if err != nil { - return Dec{}, ErrInvalidDecString.Wrap(err.Error()) - } +type SetupConstraint func(Dec) error - d1 := Dec{*d} - if d1.dec.Form == apd.Infinite { - return d1, ErrInfiniteString.Wrapf(s) +// AssertNotNegative greater or equal 0 +func AssertNotNegative() SetupConstraint { + return func(d Dec) error { + if d.IsNegative() { + return ErrInvalidDecString.Wrap("is negative") + } + return nil } - - return d1, nil } -func NewNonNegativeDecFromString(s string) (Dec, error) { - d, err := NewDecFromString(s) - if err != nil { - return Dec{}, ErrInvalidDecString.Wrap(err.Error()) - } - if d.IsNegative() { - return Dec{}, ErrInvalidDecString.Wrapf("expected a non-negative decimal, got %s", s) +// AssertGreaterThanZero greater than 0 +func AssertGreaterThanZero() SetupConstraint { + return func(d Dec) error { + if !d.IsPositive() { + return ErrInvalidDecString.Wrap("is negative") + } + return nil } - return d, nil } -func NewNonNegativeFixedDecFromString(s string, max uint32) (Dec, error) { - d, err := NewNonNegativeDecFromString(s) - if err != nil { - return Dec{}, err - } - if d.NumDecimalPlaces() > max { - return Dec{}, fmt.Errorf("%s exceeds maximum decimal places: %d", s, max) +// AssertMaxDecimals limit the decimal places +func AssertMaxDecimals(max uint32) SetupConstraint { + return func(d Dec) error { + if d.NumDecimalPlaces() > max { + return ErrInvalidDecString.Wrapf("exceeds maximum decimal places: %d", max) + } + return nil } - return d, nil } -func NewPositiveDecFromString(s string) (Dec, error) { - d, err := NewDecFromString(s) +// NewDecFromString constructor +func NewDecFromString(s string, c ...SetupConstraint) (Dec, error) { + d, _, err := apd.NewFromString(s) if err != nil { return Dec{}, ErrInvalidDecString.Wrap(err.Error()) } - if !d.IsPositive() || !d.IsFinite() { - return Dec{}, ErrInvalidDecString.Wrapf("expected a positive decimal, got %s", s) + + switch d.Form { + case apd.NaN, apd.NaNSignaling: + return Dec{}, ErrInvalidDecString.Wrap("not a number") + case apd.Infinite: + return Dec{}, ErrInfiniteString.Wrapf(s) + default: + result := Dec{*d} + for _, v := range c { + if err := v(result); err != nil { + return Dec{}, err + } + } + return result, nil } - return d, nil } -func NewPositiveFixedDecFromString(s string, max uint32) (Dec, error) { - d, err := NewPositiveDecFromString(s) - if err != nil { - return Dec{}, err - } - if d.NumDecimalPlaces() > max { - return Dec{}, fmt.Errorf("%s exceeds maximum decimal places: %d", s, max) - } - return d, nil +// NewNonNegativeDecFromString constructor +func NewNonNegativeDecFromString(s string, c ...SetupConstraint) (Dec, error) { + return NewDecFromString(s, append(c, AssertNotNegative())...) } func NewDecFromInt64(x int64) Dec { @@ -216,7 +214,7 @@ func (x Dec) BigInt() (*big.Int, error) { // Panics if x is bigger the SDK Int max value func (x Dec) SdkIntTrim() Int { y, _ := x.Reduce() - var r = y.dec.Coeff + r := y.dec.Coeff if y.dec.Exponent != 0 { decs := big.NewInt(10) if y.dec.Exponent > 0 { diff --git a/math/dec_rapid_test.go b/math/dec_rapid_test.go new file mode 100644 index 000000000000..c860a8aea16e --- /dev/null +++ b/math/dec_rapid_test.go @@ -0,0 +1,598 @@ +package math + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +// Rapid is a Go library for property-based testing. +func TestDecWithRapid(t *testing.T) { + // Property tests + t.Run("TestNewDecFromInt64", rapid.MakeCheck(testDecInt64)) + + // Properties about *FromString functions + t.Run("TestInvalidNewDecFromString", rapid.MakeCheck(testInvalidNewDecFromString)) + t.Run("TestInvalidNewNonNegativeDecFromString", rapid.MakeCheck(testInvalidNewNonNegativeDecFromString)) + t.Run("TestInvalidNewNonNegativeFixedDecFromString", rapid.MakeCheck(testInvalidNewNonNegativeFixedDecFromString)) + t.Run("TestInvalidNewPositiveDecFromString", rapid.MakeCheck(testInvalidNewPositiveDecFromString)) + t.Run("TestInvalidNewPositiveFixedDecFromString", rapid.MakeCheck(testInvalidNewPositiveFixedDecFromString)) + + // Properties about addition + t.Run("TestAddLeftIdentity", rapid.MakeCheck(testAddLeftIdentity)) + t.Run("TestAddRightIdentity", rapid.MakeCheck(testAddRightIdentity)) + t.Run("TestAddCommutative", rapid.MakeCheck(testAddCommutative)) + t.Run("TestAddAssociative", rapid.MakeCheck(testAddAssociative)) + + // Properties about subtraction + t.Run("TestSubRightIdentity", rapid.MakeCheck(testSubRightIdentity)) + t.Run("TestSubZero", rapid.MakeCheck(testSubZero)) + + // Properties about multiplication + t.Run("TestMulLeftIdentity", rapid.MakeCheck(testMulLeftIdentity)) + t.Run("TestMulRightIdentity", rapid.MakeCheck(testMulRightIdentity)) + t.Run("TestMulCommutative", rapid.MakeCheck(testMulCommutative)) + t.Run("TestMulAssociative", rapid.MakeCheck(testMulAssociative)) + t.Run("TestZeroIdentity", rapid.MakeCheck(testMulZero)) + + // Properties about division + t.Run("TestDivisionBySelf", rapid.MakeCheck(testSelfQuo)) + t.Run("TestDivisionByOne", rapid.MakeCheck(testQuoByOne)) + + // Properties combining operations + t.Run("TestSubAdd", rapid.MakeCheck(testSubAdd)) + t.Run("TestAddSub", rapid.MakeCheck(testAddSub)) + t.Run("TestMulQuoA", rapid.MakeCheck(testMulQuoA)) + t.Run("TestMulQuoB", rapid.MakeCheck(testMulQuoB)) + t.Run("TestMulQuoExact", rapid.MakeCheck(testMulQuoExact)) + t.Run("TestQuoMulExact", rapid.MakeCheck(testQuoMulExact)) + + // Properties about comparison and equality + t.Run("TestCmpInverse", rapid.MakeCheck(testCmpInverse)) + t.Run("TestEqualCommutative", rapid.MakeCheck(testEqualCommutative)) + + // Properties about tests on a single Dec + t.Run("TestIsZero", rapid.MakeCheck(testIsZero)) + t.Run("TestIsNegative", rapid.MakeCheck(testIsNegative)) + t.Run("TestIsPositive", rapid.MakeCheck(testIsPositive)) + t.Run("TestNumDecimalPlaces", rapid.MakeCheck(testNumDecimalPlaces)) + + // Unit tests + zero := Dec{} + one := NewDecFromInt64(1) + two := NewDecFromInt64(2) + three := NewDecFromInt64(3) + four := NewDecFromInt64(4) + five := NewDecFromInt64(5) + minusOne := NewDecFromInt64(-1) + + onePointOneFive, err := NewDecFromString("1.15") + require.NoError(t, err) + twoPointThreeFour, err := NewDecFromString("2.34") + require.NoError(t, err) + threePointFourNine, err := NewDecFromString("3.49") + require.NoError(t, err) + onePointFourNine, err := NewDecFromString("1.49") + require.NoError(t, err) + minusFivePointZero, err := NewDecFromString("-5.0") + require.NoError(t, err) + + twoThousand := NewDecFinite(2, 3) + require.True(t, twoThousand.Equal(NewDecFromInt64(2000))) + + res, err := two.Add(zero) + require.NoError(t, err) + require.True(t, res.Equal(two)) + + res, err = five.Sub(two) + require.NoError(t, err) + require.True(t, res.Equal(three)) + + res, err = SafeSubBalance(five, two) + require.NoError(t, err) + require.True(t, res.Equal(three)) + + _, err = SafeSubBalance(two, five) + require.Error(t, err, "Expected insufficient funds error") + + res, err = SafeAddBalance(three, two) + require.NoError(t, err) + require.True(t, res.Equal(five)) + + _, err = SafeAddBalance(minusFivePointZero, five) + require.Error(t, err, "Expected ErrInvalidRequest") + + res, err = four.Quo(two) + require.NoError(t, err) + require.True(t, res.Equal(two)) + + res, err = five.QuoInteger(two) + require.NoError(t, err) + require.True(t, res.Equal(two)) + + res, err = five.Rem(two) + require.NoError(t, err) + require.True(t, res.Equal(one)) + + x, err := four.Int64() + require.NoError(t, err) + require.Equal(t, int64(4), x) + + require.Equal(t, "5", five.String()) + + res, err = onePointOneFive.Add(twoPointThreeFour) + require.NoError(t, err) + require.True(t, res.Equal(threePointFourNine)) + + res, err = threePointFourNine.Sub(two) + require.NoError(t, err) + require.True(t, res.Equal(onePointFourNine)) + + res, err = minusOne.Sub(four) + require.NoError(t, err) + require.True(t, res.Equal(minusFivePointZero)) + + require.True(t, zero.IsZero()) + require.False(t, zero.IsPositive()) + require.False(t, zero.IsNegative()) + + require.False(t, one.IsZero()) + require.True(t, one.IsPositive()) + require.False(t, one.IsNegative()) + + require.False(t, minusOne.IsZero()) + require.False(t, minusOne.IsPositive()) + require.True(t, minusOne.IsNegative()) + + res, err = one.MulExact(two) + require.NoError(t, err) + require.True(t, res.Equal(two)) +} + +var genDec *rapid.Generator[Dec] = rapid.Custom(func(t *rapid.T) Dec { + f := rapid.Float64().Draw(t, "f") + dec, err := NewDecFromString(fmt.Sprintf("%g", f)) + require.NoError(t, err) + return dec +}) + +// A Dec value and the float used to create it +type floatAndDec struct { + float float64 + dec Dec +} + +// Generate a Dec value along with the float used to create it +var genFloatAndDec *rapid.Generator[floatAndDec] = rapid.Custom(func(t *rapid.T) floatAndDec { + f := rapid.Float64().Draw(t, "f") + dec, err := NewDecFromString(fmt.Sprintf("%g", f)) + require.NoError(t, err) + return floatAndDec{f, dec} +}) + +// Property: n == NewDecFromInt64(n).Int64() +func testDecInt64(t *rapid.T) { + nIn := rapid.Int64().Draw(t, "n") + nOut, err := NewDecFromInt64(nIn).Int64() + + require.NoError(t, err) + require.Equal(t, nIn, nOut) +} + +// Property: invalid_number_string(s) => NewDecFromString(s) == err +func testInvalidNewDecFromString(t *rapid.T) { + s := rapid.StringMatching("[[:alpha:]]+").Draw(t, "s") + _, err := NewDecFromString(s) + require.Error(t, err) +} + +// Property: invalid_number_string(s) || IsNegative(s) +// => NewNonNegativeDecFromString(s) == err +func testInvalidNewNonNegativeDecFromString(t *rapid.T) { + s := rapid.OneOf( + rapid.StringMatching("[[:alpha:]]+"), + rapid.StringMatching(`^-\d*\.?\d+$`).Filter( + func(s string) bool { return !strings.HasPrefix(s, "-0") && !strings.HasPrefix(s, "-.0") }, + ), + ).Draw(t, "s") + _, err := NewNonNegativeDecFromString(s) + require.Error(t, err) +} + +// Property: invalid_number_string(s) || IsNegative(s) || NumDecimals(s) > n +// => NewNonNegativeFixedDecFromString(s, n) == err +func testInvalidNewNonNegativeFixedDecFromString(t *rapid.T) { + n := rapid.Uint32Range(0, 999).Draw(t, "n") + s := rapid.OneOf( + rapid.StringMatching("[[:alpha:]]+"), + rapid.StringMatching(`^-\d*\.?\d+$`).Filter( + func(s string) bool { return !strings.HasPrefix(s, "-0") && !strings.HasPrefix(s, "-.0") }, + ), + rapid.StringMatching(fmt.Sprintf(`\d*\.\d{%d,}`, n+1)), + ).Draw(t, "s") + _, err := NewDecFromString(s, AssertNotNegative(), AssertMaxDecimals(n)) + require.Error(t, err) +} + +// Property: invalid_number_string(s) || IsNegative(s) || IsZero(s) +// => NewPositiveDecFromString(s) == err +func testInvalidNewPositiveDecFromString(t *rapid.T) { + s := rapid.OneOf( + rapid.StringMatching("[[:alpha:]]+"), + rapid.StringMatching(`^-\d*\.?\d+|0$`), + ).Draw(t, "s") + _, err := NewDecFromString(s, AssertGreaterThanZero()) + require.Error(t, err) +} + +// Property: invalid_number_string(s) || IsNegative(s) || IsZero(s) || NumDecimals(s) > n +// => NewPositiveFixedDecFromString(s) == err +func testInvalidNewPositiveFixedDecFromString(t *rapid.T) { + n := rapid.Uint32Range(0, 999).Draw(t, "n") + s := rapid.OneOf( + rapid.StringMatching("[[:alpha:]]+"), + rapid.StringMatching(`^-\d*\.?\d+|0$`), + rapid.StringMatching(fmt.Sprintf(`\d*\.\d{%d,}`, n+1)), + ).Draw(t, "s") + _, err := NewDecFromString(s, AssertGreaterThanZero(), AssertMaxDecimals(n)) + require.Error(t, err) +} + +// Property: 0 + a == a +func testAddLeftIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := NewDecFromInt64(0) + + b, err := zero.Add(a) + require.NoError(t, err) + + require.True(t, a.Equal(b)) +} + +// Property: a + 0 == a +func testAddRightIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := NewDecFromInt64(0) + + b, err := a.Add(zero) + require.NoError(t, err) + + require.True(t, a.Equal(b)) +} + +// Property: a + b == b + a +func testAddCommutative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + + c, err := a.Add(b) + require.NoError(t, err) + + d, err := b.Add(a) + require.NoError(t, err) + + require.True(t, c.Equal(d)) +} + +// Property: (a + b) + c == a + (b + c) +func testAddAssociative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + c := genDec.Draw(t, "c") + + // (a + b) + c + d, err := a.Add(b) + require.NoError(t, err) + + e, err := d.Add(c) + require.NoError(t, err) + + // a + (b + c) + f, err := b.Add(c) + require.NoError(t, err) + + g, err := a.Add(f) + require.NoError(t, err) + + require.True(t, e.Equal(g)) +} + +// Property: a - 0 == a +func testSubRightIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := NewDecFromInt64(0) + + b, err := a.Sub(zero) + require.NoError(t, err) + + require.True(t, a.Equal(b)) +} + +// Property: a - a == 0 +func testSubZero(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := NewDecFromInt64(0) + + b, err := a.Sub(a) + require.NoError(t, err) + + require.True(t, b.Equal(zero)) +} + +// Property: 1 * a == a +func testMulLeftIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + one := NewDecFromInt64(1) + + b, err := one.Mul(a) + require.NoError(t, err) + + require.True(t, a.Equal(b)) +} + +// Property: a * 1 == a +func testMulRightIdentity(t *rapid.T) { + a := genDec.Draw(t, "a") + one := NewDecFromInt64(1) + + b, err := a.Mul(one) + require.NoError(t, err) + + require.True(t, a.Equal(b)) +} + +// Property: a * b == b * a +func testMulCommutative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + + c, err := a.Mul(b) + require.NoError(t, err) + + d, err := b.Mul(a) + require.NoError(t, err) + + require.True(t, c.Equal(d)) +} + +// Property: (a * b) * c == a * (b * c) +func testMulAssociative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + c := genDec.Draw(t, "c") + + // (a * b) * c + d, err := a.Mul(b) + require.NoError(t, err) + + e, err := d.Mul(c) + require.NoError(t, err) + + // a * (b * c) + f, err := b.Mul(c) + require.NoError(t, err) + + g, err := a.Mul(f) + require.NoError(t, err) + + require.True(t, e.Equal(g)) +} + +// Property: (a - b) + b == a +func testSubAdd(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + + c, err := a.Sub(b) + require.NoError(t, err) + + d, err := c.Add(b) + require.NoError(t, err) + + require.True(t, a.Equal(d)) +} + +// Property: (a + b) - b == a +func testAddSub(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + + c, err := a.Add(b) + require.NoError(t, err) + + d, err := c.Sub(b) + require.NoError(t, err) + + require.True(t, a.Equal(d)) +} + +// Property: a * 0 = 0 +func testMulZero(t *rapid.T) { + a := genDec.Draw(t, "a") + zero := Dec{} + + c, err := a.Mul(zero) + require.NoError(t, err) + require.True(t, c.IsZero()) +} + +// Property: a/a = 1 +func testSelfQuo(t *rapid.T) { + decNotZero := func(d Dec) bool { return !d.IsZero() } + a := genDec.Filter(decNotZero).Draw(t, "a") + one := NewDecFromInt64(1) + + b, err := a.Quo(a) + require.NoError(t, err) + require.True(t, one.Equal(b)) +} + +// Property: a/1 = a +func testQuoByOne(t *rapid.T) { + a := genDec.Draw(t, "a") + one := NewDecFromInt64(1) + + b, err := a.Quo(one) + require.NoError(t, err) + require.True(t, a.Equal(b)) +} + +// Property: (a * b) / a == b +func testMulQuoA(t *rapid.T) { + decNotZero := func(d Dec) bool { return !d.IsZero() } + a := genDec.Filter(decNotZero).Draw(t, "a") + b := genDec.Draw(t, "b") + + c, err := a.Mul(b) + require.NoError(t, err) + + d, err := c.Quo(a) + require.NoError(t, err) + + require.True(t, b.Equal(d)) +} + +// Property: (a * b) / b == a +func testMulQuoB(t *rapid.T) { + decNotZero := func(d Dec) bool { return !d.IsZero() } + a := genDec.Draw(t, "a") + b := genDec.Filter(decNotZero).Draw(t, "b") + + c, err := a.Mul(b) + require.NoError(t, err) + + d, err := c.Quo(b) + require.NoError(t, err) + + require.True(t, a.Equal(d)) +} + +// Property: (a * 10^b) / 10^b == a using MulExact and QuoExact +// and a with no more than b decimal places (b <= 32). +func testMulQuoExact(t *rapid.T) { + b := rapid.Uint32Range(0, 32).Draw(t, "b") + decPrec := func(d Dec) bool { return d.NumDecimalPlaces() <= b } + a := genDec.Filter(decPrec).Draw(t, "a") + + c := NewDecFinite(1, int32(b)) + + d, err := a.MulExact(c) + require.NoError(t, err) + + e, err := d.QuoExact(c) + require.NoError(t, err) + + require.True(t, a.Equal(e)) +} + +// Property: (a / b) * b == a using QuoExact and MulExact and +// a as an integer. +func testQuoMulExact(t *rapid.T) { + a := rapid.Uint64().Draw(t, "a") + aDec, err := NewDecFromString(fmt.Sprintf("%d", a)) + require.NoError(t, err) + b := rapid.Uint32Range(0, 32).Draw(t, "b") + c := NewDecFinite(1, int32(b)) + + require.NoError(t, err) + + d, err := aDec.QuoExact(c) + require.NoError(t, err) + + e, err := d.MulExact(c) + require.NoError(t, err) + + require.True(t, aDec.Equal(e)) +} + +// Property: Cmp(a, b) == -Cmp(b, a) +func testCmpInverse(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + + require.Equal(t, a.Cmp(b), -b.Cmp(a)) +} + +// Property: Equal(a, b) == Equal(b, a) +func testEqualCommutative(t *rapid.T) { + a := genDec.Draw(t, "a") + b := genDec.Draw(t, "b") + + require.Equal(t, a.Equal(b), b.Equal(a)) +} + +// Property: isZero(f) == isZero(NewDecFromString(f.String())) +func testIsZero(t *rapid.T) { + floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") + f, dec := floatAndDec.float, floatAndDec.dec + + require.Equal(t, f == 0, dec.IsZero()) +} + +// Property: isNegative(f) == isNegative(NewDecFromString(f.String())) +func testIsNegative(t *rapid.T) { + floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") + f, dec := floatAndDec.float, floatAndDec.dec + + require.Equal(t, f < 0, dec.IsNegative()) +} + +// Property: isPositive(f) == isPositive(NewDecFromString(f.String())) +func testIsPositive(t *rapid.T) { + floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") + f, dec := floatAndDec.float, floatAndDec.dec + + require.Equal(t, f > 0, dec.IsPositive()) +} + +// Property: floatDecimalPlaces(f) == NumDecimalPlaces(NewDecFromString(f.String())) +func testNumDecimalPlaces(t *rapid.T) { + floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") + f, dec := floatAndDec.float, floatAndDec.dec + + require.Equal(t, floatDecimalPlaces(t, f), dec.NumDecimalPlaces()) +} + +func floatDecimalPlaces(t *rapid.T, f float64) uint32 { + reScientific := regexp.MustCompile(`^\-?(?:[[:digit:]]+(?:\.([[:digit:]]+))?|\.([[:digit:]]+))(?:e?(?:\+?([[:digit:]]+)|(-[[:digit:]]+)))?$`) + fStr := fmt.Sprintf("%g", f) + matches := reScientific.FindAllStringSubmatch(fStr, 1) + if len(matches) != 1 { + t.Fatalf("Didn't match float: %g", f) + } + + // basePlaces is the number of decimal places in the decimal part of the + // string + basePlaces := 0 + if matches[0][1] != "" { + basePlaces = len(matches[0][1]) + } else if matches[0][2] != "" { + basePlaces = len(matches[0][2]) + } + t.Logf("Base places: %d", basePlaces) + + // exp is the exponent + exp := 0 + if matches[0][3] != "" { + var err error + exp, err = strconv.Atoi(matches[0][3]) + require.NoError(t, err) + } else if matches[0][4] != "" { + var err error + exp, err = strconv.Atoi(matches[0][4]) + require.NoError(t, err) + } + + // Subtract exponent from base and check if negative + res := basePlaces - exp + if res <= 0 { + return 0 + } + + return uint32(res) +} diff --git a/math/dec_test.go b/math/dec_test.go index 94c64cc99d10..5b3f5b61ee1f 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -1,614 +1,128 @@ package math import ( - "fmt" - "regexp" - "strconv" "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "pgregory.net/rapid" ) -func TestDec(t *testing.T) { - - // Property tests - t.Run("TestNewDecFromInt64", rapid.MakeCheck(testDecInt64)) - - // Properties about *FromString functions - t.Run("TestInvalidNewDecFromString", rapid.MakeCheck(testInvalidNewDecFromString)) - t.Run("TestInvalidNewNonNegativeDecFromString", rapid.MakeCheck(testInvalidNewNonNegativeDecFromString)) - t.Run("TestInvalidNewNonNegativeFixedDecFromString", rapid.MakeCheck(testInvalidNewNonNegativeFixedDecFromString)) - t.Run("TestInvalidNewPositiveDecFromString", rapid.MakeCheck(testInvalidNewPositiveDecFromString)) - t.Run("TestInvalidNewPositiveFixedDecFromString", rapid.MakeCheck(testInvalidNewPositiveFixedDecFromString)) - - // Properties about addition - t.Run("TestAddLeftIdentity", rapid.MakeCheck(testAddLeftIdentity)) - t.Run("TestAddRightIdentity", rapid.MakeCheck(testAddRightIdentity)) - t.Run("TestAddCommutative", rapid.MakeCheck(testAddCommutative)) - t.Run("TestAddAssociative", rapid.MakeCheck(testAddAssociative)) - - // Properties about subtraction - t.Run("TestSubRightIdentity", rapid.MakeCheck(testSubRightIdentity)) - t.Run("TestSubZero", rapid.MakeCheck(testSubZero)) - - // Properties about multiplication - t.Run("TestMulLeftIdentity", rapid.MakeCheck(testMulLeftIdentity)) - t.Run("TestMulRightIdentity", rapid.MakeCheck(testMulRightIdentity)) - t.Run("TestMulCommutative", rapid.MakeCheck(testMulCommutative)) - t.Run("TestMulAssociative", rapid.MakeCheck(testMulAssociative)) - t.Run("TestZeroIdentity", rapid.MakeCheck(testMulZero)) - - // Properties about division - t.Run("TestDivisionBySelf", rapid.MakeCheck(testSelfQuo)) - t.Run("TestDivisionByOne", rapid.MakeCheck(testQuoByOne)) - - // Properties combining operations - t.Run("TestSubAdd", rapid.MakeCheck(testSubAdd)) - t.Run("TestAddSub", rapid.MakeCheck(testAddSub)) - t.Run("TestMulQuoA", rapid.MakeCheck(testMulQuoA)) - t.Run("TestMulQuoB", rapid.MakeCheck(testMulQuoB)) - t.Run("TestMulQuoExact", rapid.MakeCheck(testMulQuoExact)) - t.Run("TestQuoMulExact", rapid.MakeCheck(testQuoMulExact)) - - // Properties about comparison and equality - t.Run("TestCmpInverse", rapid.MakeCheck(testCmpInverse)) - t.Run("TestEqualCommutative", rapid.MakeCheck(testEqualCommutative)) - - // Properties about tests on a single Dec - t.Run("TestIsZero", rapid.MakeCheck(testIsZero)) - t.Run("TestIsNegative", rapid.MakeCheck(testIsNegative)) - t.Run("TestIsPositive", rapid.MakeCheck(testIsPositive)) - t.Run("TestNumDecimalPlaces", rapid.MakeCheck(testNumDecimalPlaces)) - - // Unit tests - zero := Dec{} - one := NewDecFromInt64(1) - two := NewDecFromInt64(2) - three := NewDecFromInt64(3) - four := NewDecFromInt64(4) - five := NewDecFromInt64(5) - minusOne := NewDecFromInt64(-1) - - onePointOneFive, err := NewDecFromString("1.15") - require.NoError(t, err) - twoPointThreeFour, err := NewDecFromString("2.34") - require.NoError(t, err) - threePointFourNine, err := NewDecFromString("3.49") - require.NoError(t, err) - onePointFourNine, err := NewDecFromString("1.49") - require.NoError(t, err) - minusFivePointZero, err := NewDecFromString("-5.0") - require.NoError(t, err) - - twoThousand := NewDecFinite(2, 3) - require.True(t, twoThousand.Equal(NewDecFromInt64(2000))) - - res, err := two.Add(zero) - require.NoError(t, err) - require.True(t, res.Equal(two)) - - res, err = five.Sub(two) - require.NoError(t, err) - require.True(t, res.Equal(three)) - - res, err = SafeSubBalance(five, two) - require.NoError(t, err) - require.True(t, res.Equal(three)) - - _, err = SafeSubBalance(two, five) - require.Error(t, err, "Expected insufficient funds error") - - res, err = SafeAddBalance(three, two) - require.NoError(t, err) - require.True(t, res.Equal(five)) - - _, err = SafeAddBalance(minusFivePointZero, five) - require.Error(t, err, "Expected ErrInvalidRequest") - - res, err = four.Quo(two) - require.NoError(t, err) - require.True(t, res.Equal(two)) - - res, err = five.QuoInteger(two) - require.NoError(t, err) - require.True(t, res.Equal(two)) - - res, err = five.Rem(two) - require.NoError(t, err) - require.True(t, res.Equal(one)) - - x, err := four.Int64() - require.NoError(t, err) - require.Equal(t, int64(4), x) - - require.Equal(t, "5", five.String()) - - res, err = onePointOneFive.Add(twoPointThreeFour) - require.NoError(t, err) - require.True(t, res.Equal(threePointFourNine)) - - res, err = threePointFourNine.Sub(two) - require.NoError(t, err) - require.True(t, res.Equal(onePointFourNine)) - - res, err = minusOne.Sub(four) - require.NoError(t, err) - require.True(t, res.Equal(minusFivePointZero)) - - require.True(t, zero.IsZero()) - require.False(t, zero.IsPositive()) - require.False(t, zero.IsNegative()) - - require.False(t, one.IsZero()) - require.True(t, one.IsPositive()) - require.False(t, one.IsNegative()) - - require.False(t, minusOne.IsZero()) - require.False(t, minusOne.IsPositive()) - require.True(t, minusOne.IsNegative()) - - res, err = one.MulExact(two) - require.NoError(t, err) - require.True(t, res.Equal(two)) -} - -// TODO: Think a bit more about the probability distribution of Dec -var genDec *rapid.Generator[Dec] = rapid.Custom(func(t *rapid.T) Dec { - f := rapid.Float64().Draw(t, "f") - dec, err := NewDecFromString(fmt.Sprintf("%g", f)) - require.NoError(t, err) - return dec -}) - -// A Dec value and the float used to create it -type floatAndDec struct { - float float64 - dec Dec -} - -// Generate a Dec value along with the float used to create it -var genFloatAndDec *rapid.Generator[floatAndDec] = rapid.Custom(func(t *rapid.T) floatAndDec { - f := rapid.Float64().Draw(t, "f") - dec, err := NewDecFromString(fmt.Sprintf("%g", f)) - require.NoError(t, err) - return floatAndDec{f, dec} -}) - -// Property: n == NewDecFromInt64(n).Int64() -func testDecInt64(t *rapid.T) { - nIn := rapid.Int64().Draw(t, "n") - nOut, err := NewDecFromInt64(nIn).Int64() - - require.NoError(t, err) - require.Equal(t, nIn, nOut) -} - -// Property: invalid_number_string(s) => NewDecFromString(s) == err -func testInvalidNewDecFromString(t *rapid.T) { - s := rapid.StringMatching("[[:alpha:]]+").Draw(t, "s") - _, err := NewDecFromString(s) - require.Error(t, err) -} - -// Property: invalid_number_string(s) || IsNegative(s) -// => NewNonNegativeDecFromString(s) == err -func testInvalidNewNonNegativeDecFromString(t *rapid.T) { - s := rapid.OneOf( - rapid.StringMatching("[[:alpha:]]+"), - rapid.StringMatching(`^-\d*\.?\d+$`).Filter( - func(s string) bool { return !strings.HasPrefix(s, "-0") && !strings.HasPrefix(s, "-.0") }, - ), - ).Draw(t, "s") - _, err := NewNonNegativeDecFromString(s) - require.Error(t, err) -} - -// Property: invalid_number_string(s) || IsNegative(s) || NumDecimals(s) > n -// => NewNonNegativeFixedDecFromString(s, n) == err -func testInvalidNewNonNegativeFixedDecFromString(t *rapid.T) { - n := rapid.Uint32Range(0, 999).Draw(t, "n") - s := rapid.OneOf( - rapid.StringMatching("[[:alpha:]]+"), - rapid.StringMatching(`^-\d*\.?\d+$`).Filter( - func(s string) bool { return !strings.HasPrefix(s, "-0") && !strings.HasPrefix(s, "-.0") }, - ), - rapid.StringMatching(fmt.Sprintf(`\d*\.\d{%d,}`, n+1)), - ).Draw(t, "s") - _, err := NewNonNegativeFixedDecFromString(s, n) - require.Error(t, err) -} - -// Property: invalid_number_string(s) || IsNegative(s) || IsZero(s) -// => NewPositiveDecFromString(s) == err -func testInvalidNewPositiveDecFromString(t *rapid.T) { - s := rapid.OneOf( - rapid.StringMatching("[[:alpha:]]+"), - rapid.StringMatching(`^-\d*\.?\d+|0$`), - ).Draw(t, "s") - _, err := NewPositiveDecFromString(s) - require.Error(t, err) -} - -// Property: invalid_number_string(s) || IsNegative(s) || IsZero(s) || NumDecimals(s) > n -// => NewPositiveFixedDecFromString(s) == err -func testInvalidNewPositiveFixedDecFromString(t *rapid.T) { - n := rapid.Uint32Range(0, 999).Draw(t, "n") - s := rapid.OneOf( - rapid.StringMatching("[[:alpha:]]+"), - rapid.StringMatching(`^-\d*\.?\d+|0$`), - rapid.StringMatching(fmt.Sprintf(`\d*\.\d{%d,}`, n+1)), - ).Draw(t, "s") - _, err := NewPositiveFixedDecFromString(s, n) - require.Error(t, err) -} - -// Property: 0 + a == a -func testAddLeftIdentity(t *rapid.T) { - a := genDec.Draw(t, "a") - zero := NewDecFromInt64(0) - - b, err := zero.Add(a) - require.NoError(t, err) - - require.True(t, a.Equal(b)) -} - -// Property: a + 0 == a -func testAddRightIdentity(t *rapid.T) { - a := genDec.Draw(t, "a") - zero := NewDecFromInt64(0) - - b, err := a.Add(zero) - require.NoError(t, err) - - require.True(t, a.Equal(b)) -} - -// Property: a + b == b + a -func testAddCommutative(t *rapid.T) { - a := genDec.Draw(t, "a") - b := genDec.Draw(t, "b") - - c, err := a.Add(b) - require.NoError(t, err) - - d, err := b.Add(a) - require.NoError(t, err) - - require.True(t, c.Equal(d)) -} - -// Property: (a + b) + c == a + (b + c) -func testAddAssociative(t *rapid.T) { - a := genDec.Draw(t, "a") - b := genDec.Draw(t, "b") - c := genDec.Draw(t, "c") - - // (a + b) + c - d, err := a.Add(b) - require.NoError(t, err) - - e, err := d.Add(c) - require.NoError(t, err) - - // a + (b + c) - f, err := b.Add(c) - require.NoError(t, err) - - g, err := a.Add(f) - require.NoError(t, err) - - require.True(t, e.Equal(g)) -} - -// Property: a - 0 == a -func testSubRightIdentity(t *rapid.T) { - a := genDec.Draw(t, "a") - zero := NewDecFromInt64(0) - - b, err := a.Sub(zero) - require.NoError(t, err) - - require.True(t, a.Equal(b)) -} - -// Property: a - a == 0 -func testSubZero(t *rapid.T) { - a := genDec.Draw(t, "a") - zero := NewDecFromInt64(0) - - b, err := a.Sub(a) - require.NoError(t, err) - - require.True(t, b.Equal(zero)) -} - -// Property: 1 * a == a -func testMulLeftIdentity(t *rapid.T) { - a := genDec.Draw(t, "a") - one := NewDecFromInt64(1) - - b, err := one.Mul(a) - require.NoError(t, err) - - require.True(t, a.Equal(b)) -} - -// Property: a * 1 == a -func testMulRightIdentity(t *rapid.T) { - a := genDec.Draw(t, "a") - one := NewDecFromInt64(1) - - b, err := a.Mul(one) - require.NoError(t, err) - - require.True(t, a.Equal(b)) -} - -// Property: a * b == b * a -func testMulCommutative(t *rapid.T) { - a := genDec.Draw(t, "a") - b := genDec.Draw(t, "b") - - c, err := a.Mul(b) - require.NoError(t, err) - - d, err := b.Mul(a) - require.NoError(t, err) - - require.True(t, c.Equal(d)) -} - -// Property: (a * b) * c == a * (b * c) -func testMulAssociative(t *rapid.T) { - a := genDec.Draw(t, "a") - b := genDec.Draw(t, "b") - c := genDec.Draw(t, "c") - - // (a * b) * c - d, err := a.Mul(b) - require.NoError(t, err) - - e, err := d.Mul(c) - require.NoError(t, err) - - // a * (b * c) - f, err := b.Mul(c) - require.NoError(t, err) - - g, err := a.Mul(f) - require.NoError(t, err) - - require.True(t, e.Equal(g)) -} - -// Property: (a - b) + b == a -func testSubAdd(t *rapid.T) { - a := genDec.Draw(t, "a") - b := genDec.Draw(t, "b") - - c, err := a.Sub(b) - require.NoError(t, err) - - d, err := c.Add(b) - require.NoError(t, err) - - require.True(t, a.Equal(d)) -} - -// Property: (a + b) - b == a -func testAddSub(t *rapid.T) { - a := genDec.Draw(t, "a") - b := genDec.Draw(t, "b") - - c, err := a.Add(b) - require.NoError(t, err) - - d, err := c.Sub(b) - require.NoError(t, err) - - require.True(t, a.Equal(d)) -} - -// Property: a * 0 = 0 -func testMulZero(t *rapid.T) { - a := genDec.Draw(t, "a") - zero := Dec{} - - c, err := a.Mul(zero) - require.NoError(t, err) - require.True(t, c.IsZero()) -} - -// Property: a/a = 1 -func testSelfQuo(t *rapid.T) { - decNotZero := func(d Dec) bool { return !d.IsZero() } - a := genDec.Filter(decNotZero).Draw(t, "a") - one := NewDecFromInt64(1) - - b, err := a.Quo(a) - require.NoError(t, err) - require.True(t, one.Equal(b)) -} - -// Property: a/1 = a -func testQuoByOne(t *rapid.T) { - a := genDec.Draw(t, "a") - one := NewDecFromInt64(1) - - b, err := a.Quo(one) - require.NoError(t, err) - require.True(t, a.Equal(b)) -} - -// Property: (a * b) / a == b -func testMulQuoA(t *rapid.T) { - decNotZero := func(d Dec) bool { return !d.IsZero() } - a := genDec.Filter(decNotZero).Draw(t, "a") - b := genDec.Draw(t, "b") - - c, err := a.Mul(b) - require.NoError(t, err) - - d, err := c.Quo(a) - require.NoError(t, err) - - require.True(t, b.Equal(d)) -} - -// Property: (a * b) / b == a -func testMulQuoB(t *rapid.T) { - decNotZero := func(d Dec) bool { return !d.IsZero() } - a := genDec.Draw(t, "a") - b := genDec.Filter(decNotZero).Draw(t, "b") - - c, err := a.Mul(b) - require.NoError(t, err) - - d, err := c.Quo(b) - require.NoError(t, err) - - require.True(t, a.Equal(d)) -} - -// Property: (a * 10^b) / 10^b == a using MulExact and QuoExact -// and a with no more than b decimal places (b <= 32). -func testMulQuoExact(t *rapid.T) { - b := rapid.Uint32Range(0, 32).Draw(t, "b") - decPrec := func(d Dec) bool { return d.NumDecimalPlaces() <= b } - a := genDec.Filter(decPrec).Draw(t, "a") - - c := NewDecFinite(1, int32(b)) - - d, err := a.MulExact(c) - require.NoError(t, err) - - e, err := d.QuoExact(c) - require.NoError(t, err) - - require.True(t, a.Equal(e)) -} - -// Property: (a / b) * b == a using QuoExact and MulExact and -// a as an integer. -func testQuoMulExact(t *rapid.T) { - a := rapid.Uint64().Draw(t, "a") - aDec, err := NewDecFromString(fmt.Sprintf("%d", a)) - require.NoError(t, err) - b := rapid.Uint32Range(0, 32).Draw(t, "b") - c := NewDecFinite(1, int32(b)) - - require.NoError(t, err) - - d, err := aDec.QuoExact(c) - require.NoError(t, err) - - e, err := d.MulExact(c) - require.NoError(t, err) - - require.True(t, aDec.Equal(e)) -} - -// Property: Cmp(a, b) == -Cmp(b, a) -func testCmpInverse(t *rapid.T) { - a := genDec.Draw(t, "a") - b := genDec.Draw(t, "b") - - require.Equal(t, a.Cmp(b), -b.Cmp(a)) -} - -// Property: Equal(a, b) == Equal(b, a) -func testEqualCommutative(t *rapid.T) { - a := genDec.Draw(t, "a") - b := genDec.Draw(t, "b") - - require.Equal(t, a.Equal(b), b.Equal(a)) -} - -// Property: isZero(f) == isZero(NewDecFromString(f.String())) -func testIsZero(t *rapid.T) { - floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") - f, dec := floatAndDec.float, floatAndDec.dec - - require.Equal(t, f == 0, dec.IsZero()) - -} - -// Property: isNegative(f) == isNegative(NewDecFromString(f.String())) -func testIsNegative(t *rapid.T) { - floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") - f, dec := floatAndDec.float, floatAndDec.dec - - require.Equal(t, f < 0, dec.IsNegative()) -} - -// Property: isPositive(f) == isPositive(NewDecFromString(f.String())) -func testIsPositive(t *rapid.T) { - floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") - f, dec := floatAndDec.float, floatAndDec.dec - - require.Equal(t, f > 0, dec.IsPositive()) -} - -// Property: floatDecimalPlaces(f) == NumDecimalPlaces(NewDecFromString(f.String())) -func testNumDecimalPlaces(t *rapid.T) { - floatAndDec := genFloatAndDec.Draw(t, "floatAndDec") - f, dec := floatAndDec.float, floatAndDec.dec - - require.Equal(t, floatDecimalPlaces(t, f), dec.NumDecimalPlaces()) -} - -func floatDecimalPlaces(t *rapid.T, f float64) uint32 { - reScientific := regexp.MustCompile(`^\-?(?:[[:digit:]]+(?:\.([[:digit:]]+))?|\.([[:digit:]]+))(?:e?(?:\+?([[:digit:]]+)|(-[[:digit:]]+)))?$`) - fStr := fmt.Sprintf("%g", f) - matches := reScientific.FindAllStringSubmatch(fStr, 1) - if len(matches) != 1 { - t.Fatalf("Didn't match float: %g", f) - } - - // basePlaces is the number of decimal places in the decimal part of the - // string - basePlaces := 0 - if matches[0][1] != "" { - basePlaces = len(matches[0][1]) - } else if matches[0][2] != "" { - basePlaces = len(matches[0][2]) - } - t.Logf("Base places: %d", basePlaces) - - // exp is the exponent - exp := 0 - if matches[0][3] != "" { - var err error - exp, err = strconv.Atoi(matches[0][3]) - require.NoError(t, err) - } else if matches[0][4] != "" { - var err error - exp, err = strconv.Atoi(matches[0][4]) - require.NoError(t, err) +func TestNewDecFromString(t *testing.T) { + specs := map[string]struct { + src string + constraints []SetupConstraint + exp Dec + expErr error + }{ + "simple decimal": { + src: "1", + exp: NewDecFromInt64(1), + }, + "simple negative decimal": { + src: "-1", + exp: NewDecFromInt64(-1), + }, + "valid decimal with decimal places": { + src: "1.234", + exp: NewDecFinite(1234, -3), + }, + "valid negative decimal": { + src: "-1.234", + exp: NewDecFinite(-1234, -3), + }, + "min decimal": { + src: "-" + strings.Repeat("9", 34), + exp: must(NewDecFinite(-1, 34).Add(NewDecFromInt64(1))), + }, + "max decimal": { + src: strings.Repeat("9", 34), + exp: must(NewDecFinite(1, 34).Sub(NewDecFromInt64(1))), + }, + // enable or update when precision is defined + //"decimal too small": { + // src: "-" + strings.Repeat("9", 35), + // expErr: ErrInvalidDecString, + //}, + //"decimal too big": { + // src: strings.Repeat("9", 35), + // expErr: ErrInvalidDecString, + //}, + "valid decimal with leading zero": { + src: "01234", + exp: NewDecFinite(1234, 0), + }, + "valid decimal without leading zero": { + src: ".1234", + exp: NewDecFinite(1234, -4), + }, + + "valid decimal without trailing digits": { + src: "123.", + exp: NewDecFinite(123, 0), + }, + + "valid negative decimal without leading zero": { + src: "-.1234", + exp: NewDecFinite(-1234, -4), + }, + + "valid negative decimal without trailing digits": { + src: "-123.", + exp: NewDecFinite(-123, 0), + }, + + "decimal with scientific notation": { + src: "1.23e4", + exp: NewDecFinite(123, 2), + }, + "negative decimal with scientific notation": { + src: "-1.23e4", + exp: NewDecFinite(-123, 2), + }, + "with setup constraint": { + src: "-1", + constraints: []SetupConstraint{AssertNotNegative()}, + expErr: ErrInvalidDecString, + }, + "empty string": { + src: "", + expErr: ErrInvalidDecString, + }, + "NaN": { + src: "NaN", + expErr: ErrInvalidDecString, + }, + "random string": { + src: "1foo", + expErr: ErrInvalidDecString, + }, + "Infinity": { + src: "Infinity", + expErr: ErrInfiniteString, + }, + "Inf": { + src: "Inf", + expErr: ErrInfiniteString, + }, } - - // Subtract exponent from base and check if negative - res := basePlaces - exp - if res <= 0 { - return 0 + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := NewDecFromString(spec.src, spec.constraints...) + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp.String(), got.String()) + }) } - - return uint32(res) } func TestIsFinite(t *testing.T) { a, err := NewDecFromString("1.5") require.NoError(t, err) - require.True(t, a.IsFinite()) - - b, err := NewDecFromString("NaN") - require.NoError(t, err) - - require.False(t, b.IsFinite()) } func TestReduce(t *testing.T) { @@ -712,12 +226,19 @@ func TestInfDecString(t *testing.T) { require.ErrorIs(t, err, ErrInfiniteString) } -func TestDecToLegacyDec(t *testing.T) { - dec := NewDecFromInt64(123) +//func TestDecToLegacyDec(t *testing.T) { +// dec := NewDecFromInt64(123) +// +// legacyDec, err := DecToLegacyDec(dec) +// require.NoError(t, err) +// +// expected, _ := LegacyNewDecFromStr("123.000000000000000000") +// require.True(t, legacyDec.Equal(expected)) +//} - legacyDec, err := DecToLegacyDec(dec) - require.NoError(t, err) - - expected, _ := LegacyNewDecFromStr("123.000000000000000000") - require.True(t, legacyDec.Equal(expected)) -} \ No newline at end of file +func must[T any](r T, err error) T { + if err != nil { + panic(err) + } + return r +} From 63750f05bcb1f1d21d9b22bdbe5c0c592d2bc3a5 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 14 May 2024 11:48:20 +0200 Subject: [PATCH 17/22] Add more numbers to quo benchmark --- math/dec_bench_test.go | 60 +++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/math/dec_bench_test.go b/math/dec_bench_test.go index 3bd0f0053988..98f908337c7f 100644 --- a/math/dec_bench_test.go +++ b/math/dec_bench_test.go @@ -7,22 +7,50 @@ import ( ) func BenchmarkCompareLegacyDecAndNewDec(b *testing.B) { - legacyB1 := LegacyNewDec(100) - legacyB2 := LegacyNewDec(5) - newB1 := NewDecFromInt64(100) - newB2 := NewDecFromInt64(5) - - b.Run("LegacyDec", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = legacyB1.Quo(legacyB2) - } - }) - - b.Run("NewDec", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _, _ = newB1.Quo(newB2) - } - }) + specs := map[string]struct { + dividend, divisor string + }{ + "small/ small": { + dividend: "100", divisor: "5", + }, + "big18/ small": { + dividend: "999999999999999999", divisor: "10", + }, + "big18/ big18": { + dividend: "999999999999999999", divisor: "999999999999999999", + }, + "small/ big18": { + dividend: "100", divisor: "999999999999999999", + }, + "big34/big34": { + dividend: "9999999999999999999999999999999999", divisor: "1999999999999999999999999999999999", + }, + "negative big34": { + dividend: "-9999999999999999999999999999999999", divisor: "999999999999999999999999999", + }, + "decimal small": { + dividend: "0.0000000001", divisor: "10", + }, + } + for name, spec := range specs { + b.Run(name, func(b *testing.B) { + b.Run("LegacyDec", func(b *testing.B) { + dv, ds := LegacyMustNewDecFromStr(spec.dividend), LegacyMustNewDecFromStr(spec.divisor) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = dv.Quo(ds) + } + }) + + b.Run("NewDec", func(b *testing.B) { + dv, ds := must(NewDecFromString(spec.dividend)), must(NewDecFromString(spec.divisor)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = dv.Quo(ds) + } + }) + }) + } } func BenchmarkCompareLegacyDecAndNewDecQuoInteger(b *testing.B) { From 3d59321c85de63b77263b21f3191fab354af9b61 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 14 May 2024 12:16:45 +0200 Subject: [PATCH 18/22] Apd v3 + sum benchmark --- go.mod | 3 +- go.sum | 5 --- math/dec.go | 10 +++--- math/dec_bench_test.go | 62 ++++++++++++++++++++++++++++++++++-- math/go.mod | 2 +- math/go.sum | 5 ++- math/math.go | 2 +- x/group/go.mod | 2 +- x/group/go.sum | 4 +-- x/group/internal/math/dec.go | 4 +-- 10 files changed, 74 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 7bd02584a681..d2f6817e48cd 100644 --- a/go.mod +++ b/go.mod @@ -78,8 +78,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect github.com/cespare/xxhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cockroachdb/apd/v2 v2.0.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/pebble v1.1.0 // indirect github.com/cockroachdb/redact v1.1.5 // indirect diff --git a/go.sum b/go.sum index a208bbfd4dd1..202dc64474ee 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,6 @@ cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= -cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= -cosmossdk.io/math v1.3.0/go.mod h1:vnRTxewy+M7BtXBNFybkuhSH4WfedVAAnERHgVFhp3k= cosmossdk.io/store v1.1.0 h1:LnKwgYMc9BInn9PhpTFEQVbL9UK475G2H911CGGnWHk= cosmossdk.io/store v1.1.0/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= cosmossdk.io/x/accounts/defaults/lockup v0.0.0-20240417181816-5e7aae0db1f5 h1:eb0kcGyaYHSS0do7+MIWg7UKlskSH01biRNENbm/zDA= @@ -80,9 +78,6 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= -github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= diff --git a/math/dec.go b/math/dec.go index f3bd7e175c8c..3ab7d4718204 100644 --- a/math/dec.go +++ b/math/dec.go @@ -4,7 +4,7 @@ import ( "math/big" "cosmossdk.io/errors" - "github.com/cockroachdb/apd/v2" + "github.com/cockroachdb/apd/v3" ) // Dec is a wrapper struct around apd.Decimal that does no mutation of apd.Decimal's when performing @@ -216,19 +216,19 @@ func (x Dec) SdkIntTrim() Int { y, _ := x.Reduce() r := y.dec.Coeff if y.dec.Exponent != 0 { - decs := big.NewInt(10) + decs := apd.NewBigInt(10) if y.dec.Exponent > 0 { - decs.Exp(decs, big.NewInt(int64(y.dec.Exponent)), nil) + decs.Exp(decs, apd.NewBigInt(int64(y.dec.Exponent)), nil) r.Mul(&y.dec.Coeff, decs) } else { - decs.Exp(decs, big.NewInt(int64(-y.dec.Exponent)), nil) + decs.Exp(decs, apd.NewBigInt(int64(-y.dec.Exponent)), nil) r.Quo(&y.dec.Coeff, decs) } } if x.dec.Negative { r.Neg(&r) } - return NewIntFromBigInt(&r) + return NewIntFromBigInt(r.MathBigInt()) } func (x Dec) String() string { diff --git a/math/dec_bench_test.go b/math/dec_bench_test.go index 98f908337c7f..fa37c84a5121 100644 --- a/math/dec_bench_test.go +++ b/math/dec_bench_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" ) -func BenchmarkCompareLegacyDecAndNewDec(b *testing.B) { +func BenchmarkCompareLegacyDecAndNewDecQuotient(b *testing.B) { specs := map[string]struct { dividend, divisor string }{ @@ -16,13 +16,19 @@ func BenchmarkCompareLegacyDecAndNewDec(b *testing.B) { "big18/ small": { dividend: "999999999999999999", divisor: "10", }, - "big18/ big18": { + "self18/ self18": { dividend: "999999999999999999", divisor: "999999999999999999", }, + "big18/ big18": { + dividend: "888888888888888888", divisor: "444444444444444444", + }, + "decimal18b/ decimal18c": { + dividend: "8.88888888888888888", divisor: "4.1234567890123", + }, "small/ big18": { dividend: "100", divisor: "999999999999999999", }, - "big34/big34": { + "big34/ big34": { dividend: "9999999999999999999999999999999999", divisor: "1999999999999999999999999999999999", }, "negative big34": { @@ -31,6 +37,9 @@ func BenchmarkCompareLegacyDecAndNewDec(b *testing.B) { "decimal small": { dividend: "0.0000000001", divisor: "10", }, + "decimal small/decimal small ": { + dividend: "0.0000000001", divisor: "0.0001", + }, } for name, spec := range specs { b.Run(name, func(b *testing.B) { @@ -53,6 +62,53 @@ func BenchmarkCompareLegacyDecAndNewDec(b *testing.B) { } } +func BenchmarkCompareLegacyDecAndNewDecSum(b *testing.B) { + specs := map[string]struct { + summands []string + }{ + "1+2": { + summands: []string{"1", "2"}, + }, + "growing numbers": { + summands: []string{"1", "100", "1000", "100000", "10000000", "10000000000", "10000000000000", "100000000000000000"}, + }, + "decimals": { + summands: []string{"0.1", "0.01", "0.001", "0.000001", "0.00000001", "0.00000000001", "0.00000000000001", "0.000000000000000001"}, + }, + } + for name, spec := range specs { + b.Run(name, func(b *testing.B) { + b.Run("LegacyDec", func(b *testing.B) { + summands := make([]LegacyDec, len(spec.summands)) + for i, s := range spec.summands { + summands[i] = LegacyMustNewDecFromStr(s) + } + sum := LegacyNewDec(0) + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, s := range summands { + sum = sum.Add(s) + } + } + }) + + b.Run("NewDec", func(b *testing.B) { + summands := make([]Dec, len(spec.summands)) + for i, s := range spec.summands { + summands[i] = must(NewDecFromString(s)) + } + sum := NewDecFromInt64(0) + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, s := range summands { + sum, _ = sum.Add(s) + } + } + }) + }) + } +} + func BenchmarkCompareLegacyDecAndNewDecQuoInteger(b *testing.B) { legacyB1 := LegacyNewDec(100) newB1 := NewDecFromInt64(100) diff --git a/math/go.mod b/math/go.mod index 4130dd995dd5..bc3e2b925527 100644 --- a/math/go.mod +++ b/math/go.mod @@ -5,6 +5,7 @@ go 1.21 toolchain go1.22.2 require ( + github.com/cockroachdb/apd/v3 v3.2.1 github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 sigs.k8s.io/yaml v1.4.0 @@ -32,7 +33,6 @@ require ( require ( cosmossdk.io/errors v1.0.1 - github.com/cockroachdb/apd/v2 v2.0.2 github.com/cosmos/cosmos-sdk v0.50.5 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/math/go.sum b/math/go.sum index b80eccfffbe9..178bb12faea9 100644 --- a/math/go.sum +++ b/math/go.sum @@ -6,8 +6,8 @@ github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipus github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= -github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/cometbft/cometbft v0.38.5 h1:4lOcK5VTPrfbLOhNHmPYe6c7eDXHtBdMCQuKbAfFJdU= github.com/cometbft/cometbft v0.38.5/go.mod h1:0tqKin+KQs8zDwzYD8rPHzSBIDNPuB4NrwwGDNb/hUg= github.com/cosmos/cosmos-sdk v0.50.5 h1:MOEi+DKYgW67YaPgB+Pf+nHbD3V9S/ayitRKJYLfGIA= @@ -36,7 +36,6 @@ github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mo github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc h1:8bQZVK1X6BJR/6nYUPxQEP+ReTsceJTKizeuwjWOPUA= github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= diff --git a/math/math.go b/math/math.go index 43358b2c2bfd..bfee6a5f46e3 100644 --- a/math/math.go +++ b/math/math.go @@ -5,7 +5,7 @@ import ( "fmt" "cosmossdk.io/errors" - "github.com/cockroachdb/apd/v2" + "github.com/cockroachdb/apd/v3" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) diff --git a/x/group/go.mod b/x/group/go.mod index e09fa8ad5683..f6b313606c66 100644 --- a/x/group/go.mod +++ b/x/group/go.mod @@ -18,7 +18,7 @@ require ( cosmossdk.io/x/gov v0.0.0-20230925135524-a1bc045b3190 cosmossdk.io/x/mint v0.0.0-00010101000000-000000000000 cosmossdk.io/x/staking v0.0.0-00010101000000-000000000000 - github.com/cockroachdb/apd/v2 v2.0.2 + github.com/cockroachdb/apd/v3 v3.2.1 github.com/cometbft/cometbft v0.38.7 github.com/cosmos/cosmos-db v1.0.2 github.com/cosmos/cosmos-proto v1.0.0-beta.5 diff --git a/x/group/go.sum b/x/group/go.sum index 84f755b47e5d..9fa1c319cc93 100644 --- a/x/group/go.sum +++ b/x/group/go.sum @@ -89,8 +89,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= -github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= diff --git a/x/group/internal/math/dec.go b/x/group/internal/math/dec.go index 02a0ca847678..72203a39863c 100644 --- a/x/group/internal/math/dec.go +++ b/x/group/internal/math/dec.go @@ -4,10 +4,10 @@ package math import ( "fmt" - "github.com/cockroachdb/apd/v2" - errorsmod "cosmossdk.io/errors" "cosmossdk.io/x/group/errors" + + "github.com/cockroachdb/apd/v3" ) // Dec is a wrapper struct around apd.Decimal that does no mutation of apd.Decimal's when performing From 10fc6df9cd7ece3f9acb16ae202b5acec7d392c6 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 14 May 2024 12:32:15 +0200 Subject: [PATCH 19/22] More benchs --- math/dec_bench_test.go | 125 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/math/dec_bench_test.go b/math/dec_bench_test.go index fa37c84a5121..6947f6c42b79 100644 --- a/math/dec_bench_test.go +++ b/math/dec_bench_test.go @@ -69,6 +69,16 @@ func BenchmarkCompareLegacyDecAndNewDecSum(b *testing.B) { "1+2": { summands: []string{"1", "2"}, }, + "small numbers": { + summands: []string{"123", "0.2", "3.1415", "15"}, + }, + "medium numbers": { + summands: []string{"1234.567899", "9991345552.2340134"}, + }, + "big18": { + summands: []string{"123456789012345678", "123456789012345678", "123456789012345678", "123456789012345678", "123456789012345678", "123456789012345678"}, + }, + "growing numbers": { summands: []string{"1", "100", "1000", "100000", "10000000", "10000000000", "10000000000000", "100000000000000000"}, }, @@ -109,6 +119,121 @@ func BenchmarkCompareLegacyDecAndNewDecSum(b *testing.B) { } } +func BenchmarkCompareLegacyDecAndNewDecSub(b *testing.B) { + specs := map[string]struct { + minuend string + subtrahends []string + }{ + "100 - 1 - 2": { + minuend: "100", + subtrahends: []string{"1", "2"}, + }, + "small numbers": { + minuend: "152.4013", + subtrahends: []string{"123", "0.2", "3.1415", "15"}, + }, + "10000000 - big18 numbers": { + minuend: "10000000", + subtrahends: []string{"123456789012345678", "123456789012345678", "123456789012345678", "123456789012345678", "123456789012345678", "123456789012345678"}, + }, + "10000000 - growing numbers": { + minuend: "10000000", + subtrahends: []string{"1", "100", "1000", "100000", "10000000", "10000000000", "10000000000000", "100000000000000000"}, + }, + "10000000 shrinking decimals": { + minuend: "10000000", + subtrahends: []string{"0.1", "0.01", "0.001", "0.000001", "0.00000001", "0.00000000001", "0.00000000000001", "0.000000000000000001"}, + }, + } + for name, spec := range specs { + b.Run(name, func(b *testing.B) { + b.Run("LegacyDec", func(b *testing.B) { + summands := make([]LegacyDec, len(spec.subtrahends)) + for i, s := range spec.subtrahends { + summands[i] = LegacyMustNewDecFromStr(s) + } + diff := LegacyMustNewDecFromStr(spec.minuend) + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, s := range summands { + diff = diff.Sub(s) + } + } + }) + + b.Run("NewDec", func(b *testing.B) { + summands := make([]Dec, len(spec.subtrahends)) + for i, s := range spec.subtrahends { + summands[i] = must(NewDecFromString(s)) + } + diff := must(NewDecFromString(spec.minuend)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, s := range summands { + diff, _ = diff.Sub(s) + } + } + }) + }) + } +} + +func BenchmarkCompareLegacyDecAndNewDecMul(b *testing.B) { + specs := map[string]struct { + multiplier, multiplicant string + }{ + "small/ small": { + multiplier: "100", multiplicant: "5", + }, + "big18/ small": { + multiplier: "999999999999999999", multiplicant: "10", + }, + "self18/ self18": { + multiplier: "999999999999999999", multiplicant: "999999999999999999", + }, + "big18/ big18": { + multiplier: "888888888888888888", multiplicant: "444444444444444444", + }, + "decimal18b/ decimal18c": { + multiplier: "8.88888888888888888", multiplicant: "4.1234567890123", + }, + "small/ big18": { + multiplier: "100", multiplicant: "999999999999999999", + }, + "big34/ big34": { + multiplier: "9999999999999999999999999999999999", multiplicant: "1999999999999999999999999999999999", + }, + "negative big34": { + multiplier: "-9999999999999999999999999999999999", multiplicant: "999999999999999999999999999", + }, + "decimal small": { + multiplier: "0.0000000001", multiplicant: "10", + }, + "decimal small/decimal small ": { + multiplier: "0.0000000001", multiplicant: "0.0001", + }, + } + for name, spec := range specs { + b.Run(name, func(b *testing.B) { + b.Run("LegacyDec", func(b *testing.B) { + dv, ds := LegacyMustNewDecFromStr(spec.multiplier), LegacyMustNewDecFromStr(spec.multiplicant) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = dv.Mul(ds) + } + }) + + b.Run("NewDec", func(b *testing.B) { + dv, ds := must(NewDecFromString(spec.multiplier)), must(NewDecFromString(spec.multiplicant)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = dv.Mul(ds) + } + }) + }) + } +} + func BenchmarkCompareLegacyDecAndNewDecQuoInteger(b *testing.B) { legacyB1 := LegacyNewDec(100) newB1 := NewDecFromInt64(100) From 3f8680db438f4071331693abacc91aaeaaac497d Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 14 May 2024 12:41:24 +0200 Subject: [PATCH 20/22] Marshal/unmarshal --- math/dec.go | 8 ++++++++ math/dec_bench_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/math/dec.go b/math/dec.go index 3ab7d4718204..a742aa68ad83 100644 --- a/math/dec.go +++ b/math/dec.go @@ -284,3 +284,11 @@ func (x Dec) Reduce() (Dec, int) { _, n := y.dec.Reduce(&x.dec) return y, n } + +func (d Dec) Marshal() ([]byte, error) { + return d.dec.MarshalText() +} + +func (d *Dec) Unmarshal(data []byte) error { + return d.dec.UnmarshalText(data) +} diff --git a/math/dec_bench_test.go b/math/dec_bench_test.go index 6947f6c42b79..b453a4d5f4bd 100644 --- a/math/dec_bench_test.go +++ b/math/dec_bench_test.go @@ -234,6 +234,50 @@ func BenchmarkCompareLegacyDecAndNewDecMul(b *testing.B) { } } +func BenchmarkCompareLegacyDecAndNewDecMarshalUnmarshal(b *testing.B) { + specs := map[string]struct { + src string + }{ + "small": { + src: "1", + }, + "big18": { + src: "999999999999999999", + }, + "negative big34": { + src: "9999999999999999999999999999999999", + }, + "decimal": { + src: "12345.678901234341", + }, + } + for name, spec := range specs { + b.Run(name, func(b *testing.B) { + b.Run("LegacyDec", func(b *testing.B) { + src := LegacyMustNewDecFromStr(spec.src) + b.ResetTimer() + for i := 0; i < b.N; i++ { + bz, err := src.Marshal() + require.NoError(b, err) + var d LegacyDec + require.NoError(b, d.Unmarshal(bz)) + } + }) + + b.Run("NewDec", func(b *testing.B) { + src := must(NewDecFromString(spec.src)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + bz, err := src.Marshal() + require.NoError(b, err) + var d Dec + require.NoError(b, d.Unmarshal(bz)) + } + }) + }) + } +} + func BenchmarkCompareLegacyDecAndNewDecQuoInteger(b *testing.B) { legacyB1 := LegacyNewDec(100) newB1 := NewDecFromInt64(100) From b66871ad440e9d91ceb6ce52f593d23556054d72 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Fri, 31 May 2024 16:21:04 +0200 Subject: [PATCH 21/22] Fix test --- math/dec.go | 10 ++-------- math/dec_test.go | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/math/dec.go b/math/dec.go index a742aa68ad83..acda34cb6482 100644 --- a/math/dec.go +++ b/math/dec.go @@ -99,18 +99,13 @@ func NewDecFromString(s string, c ...SetupConstraint) (Dec, error) { } } -// NewNonNegativeDecFromString constructor -func NewNonNegativeDecFromString(s string, c ...SetupConstraint) (Dec, error) { - return NewDecFromString(s, append(c, AssertNotNegative())...) -} - func NewDecFromInt64(x int64) Dec { var res Dec res.dec.SetInt64(x) return res } -// NewDecFinite returns a decimal with a value of coeff * 10^exp. +// NewDecFinite returns a decimal with a value of coeff * 10^exp precision. func NewDecFinite(coeff int64, exp int32) Dec { var res Dec res.dec.SetFinite(coeff, exp) @@ -277,8 +272,7 @@ func (x Dec) NumDecimalPlaces() uint32 { return uint32(-exp) } -// Reduce returns a copy of x with all trailing zeros removed and the number -// of trailing zeros removed. +// Reduce returns a copy of x with all trailing zeros removed func (x Dec) Reduce() (Dec, int) { y := Dec{} _, n := y.dec.Reduce(&x.dec) diff --git a/math/dec_test.go b/math/dec_test.go index 5b3f5b61ee1f..8a3ab55e82f4 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -159,7 +159,7 @@ func TestQuoExactGood(t *testing.T) { b := NewDecFinite(1, 6) c, err := a.QuoExact(b) require.NoError(t, err) - require.Equal(t, "1.000001", c.String()) + require.Equal(t, "1.000001000000000000000000000000000", c.String()) } func TestQuoExactBad(t *testing.T) { From 6d771ab9274da48656982ba7abd01830766f6bd9 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 4 Jun 2024 13:12:45 +0200 Subject: [PATCH 22/22] x --- math/dec.go | 4 +-- math/dec_rapid_test.go | 8 +++--- math/dec_test.go | 57 ++++++++++++++++++++++-------------------- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/math/dec.go b/math/dec.go index acda34cb6482..14ca6fc8b2fe 100644 --- a/math/dec.go +++ b/math/dec.go @@ -105,8 +105,8 @@ func NewDecFromInt64(x int64) Dec { return res } -// NewDecFinite returns a decimal with a value of coeff * 10^exp precision. -func NewDecFinite(coeff int64, exp int32) Dec { +// NewDecWithPrec returns a decimal with a value of coeff * 10^exp precision. +func NewDecWithPrec(coeff int64, exp int32) Dec { var res Dec res.dec.SetFinite(coeff, exp) return res diff --git a/math/dec_rapid_test.go b/math/dec_rapid_test.go index c860a8aea16e..27d6ba441f86 100644 --- a/math/dec_rapid_test.go +++ b/math/dec_rapid_test.go @@ -82,7 +82,7 @@ func TestDecWithRapid(t *testing.T) { minusFivePointZero, err := NewDecFromString("-5.0") require.NoError(t, err) - twoThousand := NewDecFinite(2, 3) + twoThousand := NewDecWithPrec(2, 3) require.True(t, twoThousand.Equal(NewDecFromInt64(2000))) res, err := two.Add(zero) @@ -200,7 +200,7 @@ func testInvalidNewNonNegativeDecFromString(t *rapid.T) { func(s string) bool { return !strings.HasPrefix(s, "-0") && !strings.HasPrefix(s, "-.0") }, ), ).Draw(t, "s") - _, err := NewNonNegativeDecFromString(s) + _, err := NewDecFromString(s, AssertNotNegative()) require.Error(t, err) } @@ -479,7 +479,7 @@ func testMulQuoExact(t *rapid.T) { decPrec := func(d Dec) bool { return d.NumDecimalPlaces() <= b } a := genDec.Filter(decPrec).Draw(t, "a") - c := NewDecFinite(1, int32(b)) + c := NewDecWithPrec(1, int32(b)) d, err := a.MulExact(c) require.NoError(t, err) @@ -497,7 +497,7 @@ func testQuoMulExact(t *rapid.T) { aDec, err := NewDecFromString(fmt.Sprintf("%d", a)) require.NoError(t, err) b := rapid.Uint32Range(0, 32).Draw(t, "b") - c := NewDecFinite(1, int32(b)) + c := NewDecWithPrec(1, int32(b)) require.NoError(t, err) diff --git a/math/dec_test.go b/math/dec_test.go index 8a3ab55e82f4..daa297a643c9 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -25,60 +25,63 @@ func TestNewDecFromString(t *testing.T) { }, "valid decimal with decimal places": { src: "1.234", - exp: NewDecFinite(1234, -3), + exp: NewDecWithPrec(1234, -3), }, "valid negative decimal": { src: "-1.234", - exp: NewDecFinite(-1234, -3), + exp: NewDecWithPrec(-1234, -3), }, "min decimal": { src: "-" + strings.Repeat("9", 34), - exp: must(NewDecFinite(-1, 34).Add(NewDecFromInt64(1))), + exp: must(NewDecWithPrec(-1, 34).Add(NewDecFromInt64(1))), }, "max decimal": { - src: strings.Repeat("9", 34), - exp: must(NewDecFinite(1, 34).Sub(NewDecFromInt64(1))), - }, - // enable or update when precision is defined - //"decimal too small": { - // src: "-" + strings.Repeat("9", 35), - // expErr: ErrInvalidDecString, - //}, - //"decimal too big": { - // src: strings.Repeat("9", 35), - // expErr: ErrInvalidDecString, - //}, + // todo: src: strings.Repeat("9", 34), + exp: must(NewDecWithPrec(1, 34).Sub(NewDecFromInt64(1))), + }, + "precision too high": { + src: "." + strings.Repeat("9", 35), + expErr: ErrInvalidDecString, + }, + "decimal too big": { + // todo: src: strings.Repeat("9", 35), // 10^100000+10 + expErr: ErrInvalidDecString, + }, + "decimal too small": { + src: strings.Repeat("9", 35), // -10^100000+0.99999999999999999... +1 + expErr: ErrInvalidDecString, + }, "valid decimal with leading zero": { src: "01234", - exp: NewDecFinite(1234, 0), + exp: NewDecWithPrec(1234, 0), }, "valid decimal without leading zero": { src: ".1234", - exp: NewDecFinite(1234, -4), + exp: NewDecWithPrec(1234, -4), }, "valid decimal without trailing digits": { src: "123.", - exp: NewDecFinite(123, 0), + exp: NewDecWithPrec(123, 0), }, "valid negative decimal without leading zero": { src: "-.1234", - exp: NewDecFinite(-1234, -4), + exp: NewDecWithPrec(-1234, -4), }, "valid negative decimal without trailing digits": { src: "-123.", - exp: NewDecFinite(-123, 0), + exp: NewDecWithPrec(-123, 0), }, "decimal with scientific notation": { src: "1.23e4", - exp: NewDecFinite(123, 2), + exp: NewDecWithPrec(123, 2), }, "negative decimal with scientific notation": { src: "-1.23e4", - exp: NewDecFinite(-123, 2), + exp: NewDecWithPrec(-123, 2), }, "with setup constraint": { src: "-1", @@ -110,7 +113,7 @@ func TestNewDecFromString(t *testing.T) { t.Run(name, func(t *testing.T) { got, gotErr := NewDecFromString(spec.src, spec.constraints...) if spec.expErr != nil { - require.ErrorIs(t, gotErr, spec.expErr) + require.ErrorIs(t, gotErr, spec.expErr, got.String()) return } require.NoError(t, gotErr) @@ -137,7 +140,7 @@ func TestReduce(t *testing.T) { func TestMulExactGood(t *testing.T) { a, err := NewDecFromString("1.000001") require.NoError(t, err) - b := NewDecFinite(1, 6) + b := NewDecWithPrec(1, 6) c, err := a.MulExact(b) require.NoError(t, err) d, err := c.Int64() @@ -148,7 +151,7 @@ func TestMulExactGood(t *testing.T) { func TestMulExactBad(t *testing.T) { a, err := NewDecFromString("1.000000000000000000000000000000000000123456789") require.NoError(t, err) - b := NewDecFinite(1, 10) + b := NewDecWithPrec(1, 10) _, err = a.MulExact(b) require.ErrorIs(t, err, ErrUnexpectedRounding) } @@ -156,7 +159,7 @@ func TestMulExactBad(t *testing.T) { func TestQuoExactGood(t *testing.T) { a, err := NewDecFromString("1000001") require.NoError(t, err) - b := NewDecFinite(1, 6) + b := NewDecWithPrec(1, 6) c, err := a.QuoExact(b) require.NoError(t, err) require.Equal(t, "1.000001000000000000000000000000000", c.String()) @@ -165,7 +168,7 @@ func TestQuoExactGood(t *testing.T) { func TestQuoExactBad(t *testing.T) { a, err := NewDecFromString("1000000000000000000000000000000000000123456789") require.NoError(t, err) - b := NewDecFinite(1, 10) + b := NewDecWithPrec(1, 10) _, err = a.QuoExact(b) require.ErrorIs(t, err, ErrUnexpectedRounding) }