From 0c9b305fc9db2894118c64883d6c2ea338cf2510 Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 18 Apr 2024 10:26:02 +0200 Subject: [PATCH 01/24] update dec w benchmark testing --- math/dec.go | 1075 +++++----------------------- math/dec_bench_test.go | 98 +++ math/dec_legacy.go | 969 +++++++++++++++++++++++++ math/dec_legacy_test.go | 784 ++++++++++++++++++++ math/dec_test.go | 1497 ++++++++++++++++----------------------- math/go.mod | 36 +- math/go.sum | 68 ++ math/math.go | 73 ++ 8 files changed, 2809 insertions(+), 1791 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 49daf378ea75..2736bf67a60f 100644 --- a/math/dec.go +++ b/math/dec.go @@ -1,971 +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 divides the current LegacyDec value by the provided LegacyDec value, truncating the result. -func (d LegacyDec) QuoTruncateMut(d2 LegacyDec) LegacyDec { - // multiply precision once before performing division - d.i.Mul(d.i, precisionReuse) - d.i.Quo(d.i, d2.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, precisionReuse) - _, rem := d.i.QuoRem(d.i, d2.i, big.NewInt(0)) - if rem.Sign() > 0 && d.IsNegative() == d2.IsNegative() || - rem.Sign() < 0 && d.IsNegative() != d2.IsNegative() { - d.i.Add(d.i, oneInt) - } - - 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 - } - - // 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 Dec{}, ErrInvalidDecString.Wrapf("expected a non-negative decimal, got %s", s) } - - 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 - -// LegacyDecsEqual return true 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 1e72e173e84f..b921435a09c7 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -1,1028 +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} +}) + +// Property: n == NewDecFromInt64(n).Int64() +func testDecInt64(t *rapid.T) { + nIn := rapid.Int64().Draw(t, "n") + nOut, err := NewDecFromInt64(nIn).Int64() - require.True(math.LegacyDecApproxEq(t, d1, d2, tol)) + 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) - // 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.True(t, a.Equal(b)) +} - require.False(math.LegacyDecApproxEq(t, d1, d2, tol)) +// 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") - return d + c, err := a.Add(b) + require.NoError(t, err) + + d, err := b.Add(a) + require.NoError(t, err) + + 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, expTruncated %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, expTruncated %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), "expTruncated %v, res %v, tc %d", tc.expAdd, resAdd, tcIndex) - s.Require().True(tc.expSub.Equal(resSub), "expTruncated %v, res %v, tc %d", tc.expSub, resSub, tcIndex) - s.Require().True(tc.expMul.Equal(resMul), "expTruncated %v, res %v, tc %d", tc.expMul, resMul, tcIndex) - s.Require().True(tc.expMulTruncate.Equal(resMulTruncate), "expTruncated %v, res %v, tc %d", tc.expMulTruncate, resMulTruncate, tcIndex) - s.Require().True(tc.expMulRoundUp.Equal(resMulRoundUp), "expTruncated %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), "expTruncated %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), "expTruncated %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), "expTruncated %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(), "expTruncated %v, res %v", expectedRoundUp, actualRoundUp) + // (a * b) * c + d, err := a.Mul(b) + require.NoError(t, err) - actualTruncate := a.MulTruncate(b) - s.Require().Equal(expectedTruncate.String(), actualTruncate.String(), "expTruncated %v, res %v", expectedRoundUp, actualTruncate) -} + e, err := d.Mul(c) + require.NoError(t, err) -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}, - } + // a * (b * c) + f, err := b.Mul(c) + require.NoError(t, err) - for tcIndex, tc := range tests { - resNeg := tc.d1.Neg().RoundInt64() - s.Require().Equal(-1*tc.exp, resNeg, "negative tc %d", tcIndex) + g, err := a.Mul(f) + require.NoError(t, err) - resPos := tc.d1.RoundInt64() - s.Require().Equal(tc.exp, resPos, "positive tc %d", tcIndex) - } + require.True(t, e.Equal(g)) } -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 testSubAdd(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.Sub(b) + require.NoError(t, err) - resPos := tc.d1.TruncateInt64() - 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) 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(), - ) +// 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)) } -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") + + c, err := a.Mul(b) + require.NoError(t, err) - 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) + d, err := c.Quo(a) + 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) - } + 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") + + c, err := a.Mul(b) + require.NoError(t, err) - // 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. + d, err := c.Quo(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) - } + 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) - baseArr := make([]math.LegacyDec, b.N) - for i := 0; i < b.N; i++ { - baseArr[i] = b1.Clone() - } - b2 := math.LegacyNewDec(4371) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - sink = baseArr[i].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) - baseArr := make([]math.LegacyDec, b.N) - for i := 0; i < b.N; i++ { - baseArr[i] = b1.Clone() - } - b2 := math.LegacyNewDec(4371) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - sink = baseArr[i].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") - require.NoError(t, err) - err = json.Unmarshal(raw, &testcases) +func TestIsFinite(t *testing.T) { + a, err := NewDecFromString("1.5") 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.True(t, a.IsFinite()) -func TestFormatDecNonDigits(t *testing.T) { - badCases := []string{ - "10.a", - "1a.10", - "p1a10.", - "0.10p", - "--10", - "12.😎😎", - "11111111111133333333333333333333333333333a", - "11111111111133333333333333333333333333333 192892", - } + b, err := NewDecFromString("NaN") + require.NoError(t, err) - 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) - } - }) - } + require.False(t, b.IsFinite()) } -func TestNegativePrecisionPanic(t *testing.T) { - require.Panics(t, func() { - math.LegacyNewDecWithPrec(10, -1) - }) +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()) } -func (s *decimalTestSuite) TestConvertToBigIntMutativeForLegacyDec() { - r := big.NewInt(30) - i := math.LegacyNewDecFromBigInt(r) +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) +} - // Compare value of BigInt & BigIntMut - s.Require().Equal(i.BigInt(), i.BigIntMut()) +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) +} - // 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 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()) +} - // 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 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) } -func TestQuoMut(t *testing.T) { - specs := map[string]struct { - dividend, divisor math.LegacyDec - expTruncated, expRoundedUp string - expPanic bool +func TestToBigInt(t *testing.T) { + i1 := "1000000000000000000000000000000000000123456789" + tcs := []struct { + intStr string + out string + isError error }{ - "0.0000000000000000001": { - dividend: math.LegacyNewDecWithPrec(1, 18), - divisor: math.LegacyMustNewDecFromStr("10"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "0.0000000000000000002": { - dividend: math.LegacyNewDecWithPrec(1, 18), - divisor: math.LegacyMustNewDecFromStr("5"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "0.0000000000000000003": { - dividend: math.LegacyNewDecWithPrec(1, 18), - divisor: math.LegacyMustNewDecFromStr("3.333333333333333"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "0.0000000000000000004": { - dividend: math.LegacyNewDecWithPrec(1, 18), - divisor: math.LegacyMustNewDecFromStr("2.5"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "0.0000000000000000005": { - dividend: math.LegacyNewDecWithPrec(1, 18), - divisor: math.LegacyMustNewDecFromStr("2"), - expRoundedUp: "0.000000000000000001", - - expTruncated: "0.000000000000000000", - }, - "0.0000000000000000006": { - dividend: math.LegacyNewDecWithPrec(1, 18), - divisor: math.LegacyMustNewDecFromStr("1.666666666666666666"), - expRoundedUp: "0.000000000000000001", - - expTruncated: "0.000000000000000000", - }, - "0.0000000000000000007": { - dividend: math.LegacyNewDecWithPrec(1, 18), - divisor: math.LegacyMustNewDecFromStr("1.428571428571429"), - expRoundedUp: "0.000000000000000001", - - expTruncated: "0.000000000000000000", - }, - "0.0000000000000000008": { - dividend: math.LegacyNewDecWithPrec(1, 18), - divisor: math.LegacyMustNewDecFromStr("1.25"), - expRoundedUp: "0.000000000000000001", - - expTruncated: "0.000000000000000000", - }, - "0.0000000000000000009": { - dividend: math.LegacyNewDecWithPrec(1, 18), - divisor: math.LegacyMustNewDecFromStr("1.111111111111111"), - expRoundedUp: "0.000000000000000001", - - expTruncated: "0.000000000000000000", - }, - "-0.0000000000000000001": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("10"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "-0.0000000000000000002": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("5"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "-0.0000000000000000003": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("3.333333333333333"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "-0.0000000000000000004": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("2.5"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "-0.0000000000000000005": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("2"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "-0.0000000000000000006": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("1.666666666666666666"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "-0.0000000000000000007": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("1.428571428571429"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "-0.0000000000000000008": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("1.25"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "-0.0000000000000000009": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("1.111111111111111"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "--0.0000000000000000001": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("-10"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "--0.0000000000000000002": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("-5"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "--0.0000000000000000003": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("-3.333333333333333"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "--0.0000000000000000004": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("-2.5"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "--0.0000000000000000005": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("-2"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "--0.0000000000000000006": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("-1.666666666666666666"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "--0.0000000000000000007": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("-1.428571428571429"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "--0.0000000000000000008": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("-1.25"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "--0.0000000000000000009": { - dividend: math.LegacyNewDecWithPrec(1, 18).Neg(), - divisor: math.LegacyMustNewDecFromStr("-1.111111111111111"), - expRoundedUp: "0.000000000000000001", - expTruncated: "0.000000000000000000", - }, - "big / small": { - dividend: math.LegacyMustNewDecFromStr("999999999999999999"), - divisor: math.LegacyNewDecWithPrec(1, 18), - expRoundedUp: "999999999999999999000000000000000000.000000000000000000", - expTruncated: "999999999999999999000000000000000000.000000000000000000", - }, - "divide by dividend": { - dividend: math.LegacyNewDecWithPrec(123, 0), - divisor: math.LegacyMustNewDecFromStr("123"), - expRoundedUp: "1.000000000000000000", - expTruncated: "1.000000000000000000", - }, - "zero divided": { - dividend: math.LegacyNewDecWithPrec(0, 0), - divisor: math.LegacyMustNewDecFromStr("1"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "zero divided by negative value": { - dividend: math.LegacyNewDecWithPrec(0, 0), - divisor: math.LegacyMustNewDecFromStr("-1"), - expRoundedUp: "0.000000000000000000", - expTruncated: "0.000000000000000000", - }, - "zero divided by zero": { - dividend: math.LegacyNewDecWithPrec(0, 0), - divisor: math.LegacyMustNewDecFromStr("0"), - expPanic: true, - }, - "divide by zero": { - dividend: math.LegacyNewDecWithPrec(1, 0), - divisor: math.LegacyMustNewDecFromStr("0"), - expPanic: true, - }, - } - for name, spec := range specs { - t.Run(name, func(t *testing.T) { - t.Run("round up", func(t *testing.T) { - t.Parallel() - if !spec.expPanic { - got := spec.dividend.Clone().QuoRoundupMut(spec.divisor.Clone()) - require.Equal(t, spec.expRoundedUp, got.String()) - return - } - require.Panics(t, func() { - _ = spec.dividend.Clone().QuoRoundupMut(spec.divisor.Clone()) - }) - }) - t.Run("truncate", func(t *testing.T) { - t.Parallel() - if !spec.expPanic { - got := spec.dividend.Clone().QuoTruncateMut(spec.divisor.Clone()) - require.Equal(t, spec.expTruncated, got.String()) - return - } - require.Panics(t, func() { - _ = spec.dividend.Clone().QuoTruncateMut(spec.divisor.Clone()) - }) - }) - }) + {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) + } } } + +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 1ac33169520aa7cf47388684304d633ac08a3f62 Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 18 Apr 2024 13:51:26 +0200 Subject: [PATCH 02/24] 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 dff520a96b1016b13a6e1c2cf1ed20d9a2cf0d26 Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 18 Apr 2024 20:22:32 +0200 Subject: [PATCH 03/24] 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 d54f7953eaaa68be4b5512a6754647050d725df2 Mon Sep 17 00:00:00 2001 From: samricotta Date: Wed, 24 Apr 2024 14:25:38 +0300 Subject: [PATCH 04/24] 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 bd795e606de16299e042d8bf3b9c0ff8faddd061 Mon Sep 17 00:00:00 2001 From: samricotta Date: Tue, 30 Apr 2024 23:51:05 +0300 Subject: [PATCH 05/24] 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 a40ab8eab816b6f827320a94b7bc973dba535d0c Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 2 May 2024 11:49:14 +0300 Subject: [PATCH 06/24] 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 1f4ed85ca904cfd1e691e8135aa6b96e31946c1d Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 2 May 2024 12:41:59 +0300 Subject: [PATCH 07/24] 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 f2c294cff004690bb7768544aa0a19bd5e511c85 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Mon, 6 May 2024 11:45:30 +0200 Subject: [PATCH 08/24] 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 b2b61dd4ef8bf9a8ca7e45d6e411a4fb9c218749 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 14 May 2024 11:48:20 +0200 Subject: [PATCH 09/24] 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 00ee00a8477d..7bed9ab9a9d6 100644 --- a/math/dec_bench_test.go +++ b/math/dec_bench_test.go @@ -5,22 +5,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 aef9429621119afec85b709a1d58d6617f2cad2c Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 14 May 2024 12:16:45 +0200 Subject: [PATCH 10/24] Apd v3 + sum benchmark --- go.mod | 5 ++- go.sum | 10 ++++-- 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 | 5 +-- x/group/go.sum | 8 +++-- x/group/internal/math/dec.go | 4 +-- 10 files changed, 87 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index b575c28b25bd..1e37e6833e09 100644 --- a/go.mod +++ b/go.mod @@ -79,6 +79,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/cockroachdb/errors v1.11.1 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/pebble v1.1.0 // indirect @@ -189,14 +190,12 @@ replace ( cosmossdk.io/core => ./core cosmossdk.io/core/testing => ./core/testing cosmossdk.io/depinject => ./depinject - cosmossdk.io/log => ./log - cosmossdk.io/store => ./store + cosmossdk.io/math => ./math cosmossdk.io/x/accounts => ./x/accounts cosmossdk.io/x/auth => ./x/auth cosmossdk.io/x/bank => ./x/bank cosmossdk.io/x/consensus => ./x/consensus cosmossdk.io/x/staking => ./x/staking - cosmossdk.io/x/tx => ./x/tx ) // Below are the long-lived replace of the Cosmos SDK diff --git a/go.sum b/go.sum index 1cb148b54b7d..c5896bf8c77f 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,14 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= -cosmossdk.io/math v1.3.0/go.mod h1:vnRTxewy+M7BtXBNFybkuhSH4WfedVAAnERHgVFhp3k= +cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= +cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= cosmossdk.io/schema v0.1.1 h1:I0M6pgI7R10nq+/HCQfbO6BsGBZA8sQy+duR1Y3aKcA= cosmossdk.io/schema v0.1.1/go.mod h1:RDAhxIeNB4bYqAlF4NBJwRrgtnciMcyyg0DOKnhNZQQ= +cosmossdk.io/store v1.1.1-0.20240418092142-896cdf1971bc h1:R9O9d75e0qZYUsVV0zzi+D7cNLnX2JrUOQNoIPaF0Bg= +cosmossdk.io/store v1.1.1-0.20240418092142-896cdf1971bc/go.mod h1:amTTatOUV3u1PsKmNb87z6/galCxrRbz9kRdJkL0DyU= +cosmossdk.io/x/tx v0.13.3 h1:Ha4mNaHmxBc6RMun9aKuqul8yHiL78EKJQ8g23Zf73g= +cosmossdk.io/x/tx v0.13.3/go.mod h1:I8xaHv0rhUdIvIdptKIqzYy27+n2+zBVaxO6fscFhys= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= @@ -68,6 +72,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/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/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 7bed9ab9a9d6..2f1afdcd911e 100644 --- a/math/dec_bench_test.go +++ b/math/dec_bench_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func BenchmarkCompareLegacyDecAndNewDec(b *testing.B) { +func BenchmarkCompareLegacyDecAndNewDecQuotient(b *testing.B) { specs := map[string]struct { dividend, divisor string }{ @@ -14,13 +14,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": { @@ -29,6 +35,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) { @@ -51,6 +60,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 0f897d363421..64ab16809432 100644 --- a/math/go.mod +++ b/math/go.mod @@ -3,6 +3,7 @@ module cosmossdk.io/math go 1.20 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 @@ -30,7 +31,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 80c70d3d79c5..d4b8b17651e6 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 v1.0.0-rc1 github.com/cometbft/cometbft/api v1.0.0-rc.1 github.com/cosmos/cosmos-db v1.0.2 @@ -182,14 +182,12 @@ require ( replace github.com/cosmos/cosmos-sdk => ../../ -// TODO remove post spinning out all modules replace ( cosmossdk.io/api => ../../api cosmossdk.io/collections => ../../collections cosmossdk.io/core => ../../core cosmossdk.io/core/testing => ../../core/testing cosmossdk.io/depinject => ../../depinject - cosmossdk.io/log => ../../log cosmossdk.io/x/accounts => ../accounts cosmossdk.io/x/accounts/defaults/multisig => ../accounts/defaults/multisig cosmossdk.io/x/auth => ../auth @@ -202,5 +200,4 @@ replace ( cosmossdk.io/x/protocolpool => ../protocolpool cosmossdk.io/x/slashing => ../slashing cosmossdk.io/x/staking => ../staking - cosmossdk.io/x/tx => ../tx ) diff --git a/x/group/go.sum b/x/group/go.sum index e3dc6325d997..5936c7622001 100644 --- a/x/group/go.sum +++ b/x/group/go.sum @@ -6,6 +6,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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/schema v0.1.1 h1:I0M6pgI7R10nq+/HCQfbO6BsGBZA8sQy+duR1Y3aKcA= @@ -16,6 +18,8 @@ cosmossdk.io/x/accounts/defaults/lockup v0.0.0-20240417181816-5e7aae0db1f5 h1:eb cosmossdk.io/x/accounts/defaults/lockup v0.0.0-20240417181816-5e7aae0db1f5/go.mod h1:drzY4oVisyWvSgpsM7ccQ7IX3efMuVIvd9Eij1Gm/6o= cosmossdk.io/x/epochs v0.0.0-20240522060652-a1ae4c3e0337 h1:GuBrfHsK3RD5vlD4DuBz3DXslR6VlnzrYmHOC3L679Q= cosmossdk.io/x/epochs v0.0.0-20240522060652-a1ae4c3e0337/go.mod h1:PhLn1pMBilyRC4GfRkoYhm+XVAYhF4adVrzut8AdpJI= +cosmossdk.io/x/tx v0.13.3 h1:Ha4mNaHmxBc6RMun9aKuqul8yHiL78EKJQ8g23Zf73g= +cosmossdk.io/x/tx v0.13.3/go.mod h1:I8xaHv0rhUdIvIdptKIqzYy27+n2+zBVaxO6fscFhys= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= @@ -83,8 +87,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 aa9043802d39c003b0acba3da0123a48b6c14c03 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 14 May 2024 12:32:15 +0200 Subject: [PATCH 11/24] 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 2f1afdcd911e..8d7feb92a9d8 100644 --- a/math/dec_bench_test.go +++ b/math/dec_bench_test.go @@ -67,6 +67,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"}, }, @@ -107,6 +117,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 d8d1c39bcdabece605abae316a5c5b32f65a8f39 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 14 May 2024 12:41:24 +0200 Subject: [PATCH 12/24] 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 8d7feb92a9d8..2055bfbe91da 100644 --- a/math/dec_bench_test.go +++ b/math/dec_bench_test.go @@ -232,6 +232,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 8af8467e7ae1404d4c18a1e21ca249af7ceb51bc Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Fri, 31 May 2024 16:21:04 +0200 Subject: [PATCH 13/24] 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 bb260f81df5bdaeb5762171b50d790a30d23dfff Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 4 Jun 2024 13:12:45 +0200 Subject: [PATCH 14/24] 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) } From e89e6ec59a1f5e7449beec29b24386c6feaf5ce1 Mon Sep 17 00:00:00 2001 From: samricotta Date: Wed, 5 Jun 2024 08:14:51 +0200 Subject: [PATCH 15/24] wip --- go.sum | 1 + math/dec.go | 4 +- math/dec_bench_test.go | 4 +- math/dec_test.go | 109 +++++++++++++++++++++++++++++++++++++++++ math/go.mod | 2 +- 5 files changed, 116 insertions(+), 4 deletions(-) diff --git a/go.sum b/go.sum index c5896bf8c77f..46eb044a0273 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ + buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.34.2-20240701160653-fedbb9acfd2f.2 h1:90/4O5QkHb8EZdA2SAhueRzYw6u5ZHCPKtReFqshnTY= buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.34.2-20240701160653-fedbb9acfd2f.2/go.mod h1:1+3gJj2NvZ1mTLAtHu+lMhOjGgQPiCKCeo+9MBww0Eo= buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.34.2-20240130113600-88ef6483f90f.2 h1:b7EEYTUHmWSBEyISHlHvXbJPqtKiHRuUignL1tsHnNQ= diff --git a/math/dec.go b/math/dec.go index 14ca6fc8b2fe..4e9d77f9e255 100644 --- a/math/dec.go +++ b/math/dec.go @@ -99,7 +99,7 @@ func NewDecFromString(s string, c ...SetupConstraint) (Dec, error) { } } -func NewDecFromInt64(x int64) Dec { +func NewDecFromInt64(x int64, c ...SetupConstraint) Dec { var res Dec res.dec.SetInt64(x) return res @@ -114,7 +114,7 @@ func NewDecWithPrec(coeff int64, exp int32) Dec { // 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) { +func (x Dec) Add(y Dec, c ...SetupConstraint) (Dec, error) { var z Dec _, err := apd.BaseContext.Add(&z.dec, &x.dec, &y.dec) return z, errors.Wrap(err, "decimal addition error") diff --git a/math/dec_bench_test.go b/math/dec_bench_test.go index 2055bfbe91da..52d79c8b4407 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 BenchmarkCompareLegacyDecAndNewDecQuotient(b *testing.B) { @@ -348,4 +350,4 @@ func BenchmarkCompareLegacySubAndDecSub(b *testing.B) { _, _ = newB1.Sub(newB2) } }) -} \ No newline at end of file +} diff --git a/math/dec_test.go b/math/dec_test.go index daa297a643c9..75fd87ebe74e 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -1,6 +1,7 @@ package math import ( + "fmt" "strings" "testing" @@ -122,6 +123,114 @@ func TestNewDecFromString(t *testing.T) { } } +func TestNewDecFromInt64(t *testing.T) { + specs := map[string]struct { + src int64 + constants []SetupConstraint + exp string + expErr error + }{ + "zero value": { + src: 0, + exp: "0", + }, + "positive value": { + src: 123, + exp: "123", + }, + "negative value": { + src: -123, + exp: "-123", + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := NewDecFromInt64(spec.src) + require.NoError(t, spec.expErr) + assert.Equal(t, spec.exp, got.String()) + }) + } +} + +func TestAdd(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + constants []SetupConstraint + exp Dec + expErr error + }{ + "zero add zero": { + // 0 + 0 = 0 + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: NewDecFromInt64(0), + }, + "zero add simple positive": { + // 0 + 123 = 123 + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(123), + }, + "simple positive add simple positive": { + // 123 + 123 = 246 + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(246), + }, + "simple negative add simple positive": { + // -123 + 123 = 0 + x: NewDecFromInt64(-123), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(0), + }, + "simple negative add simple negative": { + // -123 + -123 = -246 + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: NewDecFromInt64(-246), + }, + "valid decimal with decimal places add valid decimal with decimal places": { + // 1.234 + 1.234 = 2.468 + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(1234, -3), + exp: NewDecWithPrec(2468, -3), + }, + "valid decimal with decimal places add valid negative decimal with decimal places": { + // 1.234 + -1.234 = 0 + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: NewDecWithPrec(0, -3), + }, + "valid negative decimal with decimal places add valid negative decimal with decimal places": { + // -1.234 + -1.234 = -2.468 + x: NewDecWithPrec(-1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: NewDecWithPrec(-2468, -3), + }, + // "precision too high": { + // // 10^34 + 10^34 = 2*10^34 + // x: NewDecWithPrec(1, 35), + // y: NewDecWithPrec(1, 36), + // expErr: ErrInvalidDecString, + // }, + "decimal too small": { + // -10^34 + -10^34 = -2*10^34 + x: NewDecWithPrec(-1, 35), + y: NewDecWithPrec(-1, 35), + constraints: []SetupConstraint{AssertMaxDecimals(35)}, + expErr: ErrInvalidDecString, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := spec.x.Add(spec.y, spec.constants...) + fmt.Println(got) + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, got) + }) + } +} func TestIsFinite(t *testing.T) { a, err := NewDecFromString("1.5") require.NoError(t, err) diff --git a/math/go.mod b/math/go.mod index 64ab16809432..43d23cf5c41e 100644 --- a/math/go.mod +++ b/math/go.mod @@ -1,6 +1,6 @@ module cosmossdk.io/math -go 1.20 +go 1.21 require ( github.com/cockroachdb/apd/v3 v3.2.1 From a8cec85d921248cdff934bfc5967e32f7c864ba3 Mon Sep 17 00:00:00 2001 From: samricotta Date: Wed, 5 Jun 2024 16:54:15 +0200 Subject: [PATCH 16/24] wip --- math/dec.go | 17 +++++- math/dec_test.go | 153 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 153 insertions(+), 17 deletions(-) diff --git a/math/dec.go b/math/dec.go index 4e9d77f9e255..84c25b7c049c 100644 --- a/math/dec.go +++ b/math/dec.go @@ -117,14 +117,27 @@ func NewDecWithPrec(coeff int64, exp int32) Dec { func (x Dec) Add(y Dec, c ...SetupConstraint) (Dec, error) { var z Dec _, err := apd.BaseContext.Add(&z.dec, &x.dec, &y.dec) - return z, errors.Wrap(err, "decimal addition error") + if err != nil { + return Dec{}, ErrInvalidDecString.Wrap(err.Error()) + } + for _, constraint := range c { + if err := constraint(z); err != nil { + return Dec{}, err + } + } + return z, nil } // 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) { +func (x Dec) Sub(y Dec, c ...SetupConstraint) (Dec, error) { var z Dec _, err := apd.BaseContext.Sub(&z.dec, &x.dec, &y.dec) + for _, constraint := range c { + if err := constraint(z); err != nil { + return Dec{}, err + } + } return z, errors.Wrap(err, "decimal subtraction error") } diff --git a/math/dec_test.go b/math/dec_test.go index 75fd87ebe74e..e634e0a02d8b 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -142,6 +142,14 @@ func TestNewDecFromInt64(t *testing.T) { src: -123, exp: "-123", }, + "max value": { + src: 9223372036854775807, + exp: "9223372036854775807", + }, + "min value": { + src: -9223372036854775808, + exp: "-9223372036854775808", + }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { @@ -154,11 +162,11 @@ func TestNewDecFromInt64(t *testing.T) { func TestAdd(t *testing.T) { specs := map[string]struct { - x Dec - y Dec - constants []SetupConstraint - exp Dec - expErr error + x Dec + y Dec + constraints []SetupConstraint + exp Dec + expErr error }{ "zero add zero": { // 0 + 0 = 0 @@ -172,6 +180,12 @@ func TestAdd(t *testing.T) { y: NewDecFromInt64(123), exp: NewDecFromInt64(123), }, + "zero and simple negative": { + // 0 + -123 = -123 + x: NewDecFromInt64(0), + y: NewDecFromInt64(-123), + exp: NewDecFromInt64(-123), + }, "simple positive add simple positive": { // 123 + 123 = 246 x: NewDecFromInt64(123), @@ -196,6 +210,19 @@ func TestAdd(t *testing.T) { y: NewDecWithPrec(1234, -3), exp: NewDecWithPrec(2468, -3), }, + "valid decimal with decimal places and simple positive": { + // 1.234 + 123 = 124.234 + x: NewDecWithPrec(1234, -3), + y: NewDecFromInt64(123), + exp: NewDecWithPrec(124234, -3), + }, + "valid decimal with decimal places and simple negative": { + // 1.234 + -123 = 1.111 + x: NewDecWithPrec(1234, -3), + y: NewDecFromInt64(-123), + exp: NewDecWithPrec(111, -3), + }, + "valid decimal with decimal places add valid negative decimal with decimal places": { // 1.234 + -1.234 = 0 x: NewDecWithPrec(1234, -3), @@ -210,27 +237,123 @@ func TestAdd(t *testing.T) { }, // "precision too high": { // // 10^34 + 10^34 = 2*10^34 - // x: NewDecWithPrec(1, 35), - // y: NewDecWithPrec(1, 36), - // expErr: ErrInvalidDecString, + // x: NewDecWithPrec(1, 36), + // y: NewDecWithPrec(1, 36), + // constraints: []SetupConstraint{AssertMaxDecimals(34)}, + // expErr: ErrInvalidDecString, // }, - "decimal too small": { - // -10^34 + -10^34 = -2*10^34 - x: NewDecWithPrec(-1, 35), - y: NewDecWithPrec(-1, 35), - constraints: []SetupConstraint{AssertMaxDecimals(35)}, - expErr: ErrInvalidDecString, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := spec.x.Add(spec.y, spec.constraints...) + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr, got) + return + } + + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestSub(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + constraints []SetupConstraint + exp Dec + expErr error + }{ + "zero minus zero": { + // 0 + 0 = 0 + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: NewDecFromInt64(0), }, + "zero minus simple positive": { + // 0 - 123 = -123 + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(-123), + }, + "zero minus simple negative": { + // 0 - -123 = 123 + x: NewDecFromInt64(0), + y: NewDecFromInt64(-123), + exp: NewDecFromInt64(123), + }, + "simple positive minus simple positive": { + // 123 - 123 = 0 + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(0), + }, + "simple negative minus simple positive": { + // -123 + 123 = 0 + x: NewDecFromInt64(-123), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(-246), + }, + "simple negative minus simple negative": { + // -123 - -123 = 0 + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: NewDecFromInt64(0), + }, + "valid decimal with decimal places add valid decimal with decimal places": { + // 1.234 - 1.234 = 0.000 + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(1234, -3), + exp: NewDecWithPrec(0, -3), + }, + "valid decimal with decimal places and simple positive": { + // 1.234 - 123 = -121.766 + x: NewDecWithPrec(1234, -3), + y: NewDecFromInt64(123), + exp: NewDecWithPrec(-121766, -3), + }, + "valid decimal with decimal places and simple negative": { + // 1.234 - -123 = 1.111 + x: NewDecWithPrec(1234, -3), + y: NewDecFromInt64(-123), + exp: NewDecWithPrec(124234, -3), + }, + "valid decimal with decimal places add valid negative decimal with decimal places": { + // 1.234 - -1.234 = 2.468 + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: NewDecWithPrec(2468, -3), + }, + "valid negative decimal with decimal places add valid negative decimal with decimal places": { + // -1.234 - -1.234 = 2.468 + x: NewDecWithPrec(-1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: NewDecWithPrec(0, -3), + }, + // "precision too high": { + // // 10^34 - 10^34 = 2*10^34 + // x: NewDecWithPrec(1, 36), + // y: NewDecWithPrec(1, 36), + // constraints: []SetupConstraint{AssertMaxDecimals(34)}, + // expErr: ErrInvalidDecString, + // }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { - got, gotErr := spec.x.Add(spec.y, spec.constants...) + got, gotErr := spec.x.Sub(spec.y, spec.constraints...) fmt.Println(got) + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr, got) + return + } + require.NoError(t, gotErr) assert.Equal(t, spec.exp, got) }) } } + func TestIsFinite(t *testing.T) { a, err := NewDecFromString("1.5") require.NoError(t, err) From a03f52a496ecb4ff802af6d1c008e635dcaf61b4 Mon Sep 17 00:00:00 2001 From: samricotta Date: Thu, 6 Jun 2024 22:26:01 +0200 Subject: [PATCH 17/24] quo, quo exact, table tests, multiply tests --- math/dec.go | 32 +-- math/dec_test.go | 523 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 464 insertions(+), 91 deletions(-) diff --git a/math/dec.go b/math/dec.go index 84c25b7c049c..ffcc67a4d48a 100644 --- a/math/dec.go +++ b/math/dec.go @@ -30,7 +30,6 @@ var ( 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") ) // In cosmos-sdk#7773, decimal128 (with 34 digits of precision) was suggested for performing @@ -87,7 +86,7 @@ func NewDecFromString(s string, c ...SetupConstraint) (Dec, error) { case apd.NaN, apd.NaNSignaling: return Dec{}, ErrInvalidDecString.Wrap("not a number") case apd.Infinite: - return Dec{}, ErrInfiniteString.Wrapf(s) + return Dec{}, ErrInvalidDecString.Wrapf(s) default: result := Dec{*d} for _, v := range c { @@ -114,30 +113,25 @@ func NewDecWithPrec(coeff int64, exp int32) Dec { // 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, c ...SetupConstraint) (Dec, error) { +func (x Dec) Add(y Dec) (Dec, error) { var z Dec _, err := apd.BaseContext.Add(&z.dec, &x.dec, &y.dec) if err != nil { return Dec{}, ErrInvalidDecString.Wrap(err.Error()) } - for _, constraint := range c { - if err := constraint(z); err != nil { - return Dec{}, err - } - } + return z, nil } // 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, c ...SetupConstraint) (Dec, error) { +func (x Dec) Sub(y Dec) (Dec, error) { var z Dec _, err := apd.BaseContext.Sub(&z.dec, &x.dec, &y.dec) - for _, constraint := range c { - if err := constraint(z); err != nil { - return Dec{}, err - } + if err != nil { + return Dec{}, ErrInvalidDecString } + return z, errors.Wrap(err, "decimal subtraction error") } @@ -146,6 +140,10 @@ func (x Dec) Sub(y Dec, c ...SetupConstraint) (Dec, error) { func (x Dec) Quo(y Dec) (Dec, error) { var z Dec _, err := dec128Context.Quo(&z.dec, &x.dec, &y.dec) + if err != nil { + return Dec{}, ErrInvalidDecString + } + return z, errors.Wrap(err, "decimal quotient error") } @@ -160,6 +158,7 @@ func (x Dec) MulExact(y Dec) (Dec, error) { if condition.Rounded() { return z, ErrUnexpectedRounding } + return z, nil } @@ -168,7 +167,7 @@ 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 + return z, ErrInvalidDecString } if condition.Rounded() { return z, ErrUnexpectedRounding @@ -181,7 +180,10 @@ func (x Dec) QuoExact(y Dec) (Dec, error) { 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") + if err != nil { + return z, ErrInvalidDecString + } + return z, nil } // Rem returns the integral remainder from `x/y` (formatted as decimal128, with 34 digit precision) without diff --git a/math/dec_test.go b/math/dec_test.go index e634e0a02d8b..2cade165834f 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -103,16 +103,16 @@ func TestNewDecFromString(t *testing.T) { }, "Infinity": { src: "Infinity", - expErr: ErrInfiniteString, + expErr: ErrInvalidDecString, }, "Inf": { src: "Inf", - expErr: ErrInfiniteString, + expErr: ErrInvalidDecString, }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { - got, gotErr := NewDecFromString(spec.src, spec.constraints...) + got, gotErr := NewDecFromString(spec.src) if spec.expErr != nil { require.ErrorIs(t, gotErr, spec.expErr, got.String()) return @@ -125,10 +125,9 @@ func TestNewDecFromString(t *testing.T) { func TestNewDecFromInt64(t *testing.T) { specs := map[string]struct { - src int64 - constants []SetupConstraint - exp string - expErr error + src int64 + exp string + expErr error }{ "zero value": { src: 0, @@ -162,11 +161,10 @@ func TestNewDecFromInt64(t *testing.T) { func TestAdd(t *testing.T) { specs := map[string]struct { - x Dec - y Dec - constraints []SetupConstraint - exp Dec - expErr error + x Dec + y Dec + exp Dec + expErr error }{ "zero add zero": { // 0 + 0 = 0 @@ -242,15 +240,16 @@ func TestAdd(t *testing.T) { // constraints: []SetupConstraint{AssertMaxDecimals(34)}, // expErr: ErrInvalidDecString, // }, + + // TO DO: more edge cases For example: 1^100000 + 9^100000 , 1^100000 + 1^-1 } for name, spec := range specs { t.Run(name, func(t *testing.T) { - got, gotErr := spec.x.Add(spec.y, spec.constraints...) + got, gotErr := spec.x.Add(spec.y) if spec.expErr != nil { require.ErrorIs(t, gotErr, spec.expErr, got) return } - require.NoError(t, gotErr) assert.Equal(t, spec.exp, got) }) @@ -259,14 +258,13 @@ func TestAdd(t *testing.T) { func TestSub(t *testing.T) { specs := map[string]struct { - x Dec - y Dec - constraints []SetupConstraint - exp Dec - expErr error + x Dec + y Dec + exp Dec + expErr error }{ "zero minus zero": { - // 0 + 0 = 0 + // 0 - 0 = 0 x: NewDecFromInt64(0), y: NewDecFromInt64(0), exp: NewDecFromInt64(0), @@ -290,7 +288,7 @@ func TestSub(t *testing.T) { exp: NewDecFromInt64(0), }, "simple negative minus simple positive": { - // -123 + 123 = 0 + // -123 - 123 = 0 x: NewDecFromInt64(-123), y: NewDecFromInt64(123), exp: NewDecFromInt64(-246), @@ -307,25 +305,25 @@ func TestSub(t *testing.T) { y: NewDecWithPrec(1234, -3), exp: NewDecWithPrec(0, -3), }, - "valid decimal with decimal places and simple positive": { + "valid decimal with decimal places minus simple positive": { // 1.234 - 123 = -121.766 x: NewDecWithPrec(1234, -3), y: NewDecFromInt64(123), exp: NewDecWithPrec(-121766, -3), }, - "valid decimal with decimal places and simple negative": { + "valid decimal with decimal places minus simple negative": { // 1.234 - -123 = 1.111 x: NewDecWithPrec(1234, -3), y: NewDecFromInt64(-123), exp: NewDecWithPrec(124234, -3), }, - "valid decimal with decimal places add valid negative decimal with decimal places": { + "valid decimal with decimal places minus valid negative decimal with decimal places": { // 1.234 - -1.234 = 2.468 x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(-1234, -3), exp: NewDecWithPrec(2468, -3), }, - "valid negative decimal with decimal places add valid negative decimal with decimal places": { + "valid negative decimal with decimal places minus valid negative decimal with decimal places": { // -1.234 - -1.234 = 2.468 x: NewDecWithPrec(-1234, -3), y: NewDecWithPrec(-1234, -3), @@ -341,7 +339,7 @@ func TestSub(t *testing.T) { } for name, spec := range specs { t.Run(name, func(t *testing.T) { - got, gotErr := spec.x.Sub(spec.y, spec.constraints...) + got, gotErr := spec.x.Sub(spec.y) fmt.Println(got) if spec.expErr != nil { require.ErrorIs(t, gotErr, spec.expErr, got) @@ -354,55 +352,438 @@ func TestSub(t *testing.T) { } } -func TestIsFinite(t *testing.T) { - a, err := NewDecFromString("1.5") - require.NoError(t, err) - require.True(t, a.IsFinite()) +func TestQuo(t *testing.T) { + specs := map[string]struct { + src string + x Dec + y Dec + exp Dec + expErr error + }{ + "0 / 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + expErr: ErrInvalidDecString, + }, + " 0 / 123 = 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(0), + }, + // answer is -0 but this is not throwing an error + // "0 / -123 = 0": { + // x: NewDecFromInt64(0), + // y: NewDecFromInt64(-123), + // exp: NewDecFromInt64(-0), + // }, + "123 / 0 = 0": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + expErr: ErrInvalidDecString, + }, + "-123 / 0 = 0": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + expErr: ErrInvalidDecString, + }, + "123 / 123 = 1": { + // the answer is showing up as 0 although it should be 1. Again with 34 precision it is mismatched + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: must(NewDecFromString("1.000000000000000000000000000000000")), + }, + "-123 / 123 = 1": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(123), + exp: must(NewDecFromString("-1.000000000000000000000000000000000")), + }, + "-123 / -123 = 1": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: must(NewDecFromString("1.000000000000000000000000000000000")), + }, + "1.234 / 1.234": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(1234, -3), + exp: must(NewDecFromString("1.000000000000000000000000000000000")), + }, + "-1.234 / 1234 = -121.766": { + x: NewDecWithPrec(-1234, -3), + y: NewDecWithPrec(1234, -3), + exp: must(NewDecFromString("-1.000000000000000000000000000000000")), + }, + "1.234 / -123 = 1.111": { + x: NewDecWithPrec(1234, -3), + y: NewDecFromInt64(-123), + exp: must(NewDecFromString("-0.01003252032520325203252032520325203")), + }, + "1.234 / -1.234 = 2.468": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: must(NewDecFromString("-1.000000000000000000000000000000000")), + }, + "-1.234 / -1.234 = 1": { + x: NewDecWithPrec(-1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: must(NewDecFromString("1.000000000000000000000000000000000")), + }, + // "precision too high": { + // // 10^34 / 10^34 = 2*10^34 + // x: NewDecWithPrec(1, 36), + // y: NewDecWithPrec(1, 36), + // constraints: []SetupConstraint{AssertMaxDecimals(34)}, + // expErr: ErrInvalidDecString, + // }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := spec.x.Quo(spec.y) + fmt.Println(spec.x, spec.y, got, spec.exp) + + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, got) + }) + } } -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()) +func TestQuoExact(t *testing.T) { + specs := map[string]struct { + src string + x Dec + y Dec + exp Dec + expErr error + }{ + "0 / 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + expErr: ErrInvalidDecString, + }, + " 0 / 123 = 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(0), + }, + // answer is -0 but this is not throwing an error + // "0 / -123 = 0": { + // x: NewDecFromInt64(0), + // y: NewDecFromInt64(-123), + // exp: NewDecFromInt64(-0), + // }, + "123 / 0 = 0": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + expErr: ErrInvalidDecString, + }, + "-123 / 0 = 0": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + expErr: ErrInvalidDecString, + }, + "123 / 123 = 1": { + // the answer is showing up as 0 although it should be 1. Again with 34 precision it is mismatched + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: must(NewDecFromString("1.000000000000000000000000000000000")), + }, + "-123 / 123 = 1": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(123), + exp: must(NewDecFromString("-1.000000000000000000000000000000000")), + }, + "-123 / -123 = 1": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: must(NewDecFromString("1.000000000000000000000000000000000")), + }, + "1.234 / 1.234": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(1234, -3), + exp: must(NewDecFromString("1.000000000000000000000000000000000")), + }, + "-1.234 / 1234 = -121.766": { + x: NewDecWithPrec(-1234, -3), + y: NewDecWithPrec(1234, -3), + exp: must(NewDecFromString("-1.000000000000000000000000000000000")), + }, + "1.234 / -123 = 1.111": { + x: NewDecWithPrec(1234, -3), + y: NewDecFromInt64(-123), + expErr: ErrUnexpectedRounding, + }, + "1.234 / -1.234 = 2.468": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: must(NewDecFromString("-1.000000000000000000000000000000000")), + }, + "-1.234 / -1.234 = 1": { + x: NewDecWithPrec(-1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: must(NewDecFromString("1.000000000000000000000000000000000")), + }, + // "precision too high": { + // // 10^34 / 10^34 = 2*10^34 + // x: NewDecWithPrec(1, 36), + // y: NewDecWithPrec(1, 36), + // constraints: []SetupConstraint{AssertMaxDecimals(34)}, + // expErr: ErrInvalidDecString, + // }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := spec.x.Quo(spec.y) + fmt.Println(spec.x, spec.y, got, spec.exp) + + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, got) + }) + } } -func TestMulExactGood(t *testing.T) { - a, err := NewDecFromString("1.000001") - require.NoError(t, err) - b := NewDecWithPrec(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 TestQuoInteger(t *testing.T) { + specs := map[string]struct { + src string + x Dec + y Dec + exp Dec + expErr error + }{ + "0 / 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + expErr: ErrInvalidDecString, + }, + " 0 / 123 = 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(0), + }, + // answer is -0 but this is not throwing an error + // "0 / -123 = 0": { + // x: NewDecFromInt64(0), + // y: NewDecFromInt64(-123), + // exp: NewDecFromInt64(-0), + // }, + "123 / 0 = 0": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + expErr: ErrInvalidDecString, + }, + "-123 / 0 = 0": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + expErr: ErrInvalidDecString, + }, + "123 / 123 = 1": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(1), + }, + "-123 / 123 = -1": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(-1), + }, + "-123 / -123 = 1": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: NewDecFromInt64(1), + }, + "1.234 / 1.234": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(1234, -3), + exp: NewDecFromInt64(1), + }, + "-1.234 / 1234 = -121.766": { + x: NewDecWithPrec(-1234, -3), + y: NewDecWithPrec(1234, -3), + exp: NewDecFromInt64(-1), + }, + // "1.234 / -123 = -0": { + //-0 + // x: NewDecWithPrec(1234, -3), + // y: NewDecFromInt64(-123), + // exp: NewDecFromInt64(1), + // }, + "1.234 / -1.234 = 2.468": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: NewDecFromInt64(-1), + }, + "-1.234 / -1.234 = 1": { + x: NewDecWithPrec(-1234, -3), + y: NewDecWithPrec(-1234, -3), + exp: NewDecFromInt64(1), + }, + // "precision too high": { + // // 10^34 / 10^34 = 2*10^34 + // x: NewDecWithPrec(1, 36), + // y: NewDecWithPrec(1, 36), + // constraints: []SetupConstraint{AssertMaxDecimals(34)}, + // expErr: ErrInvalidDecString, + // }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := spec.x.QuoInteger(spec.y) + fmt.Println(spec.x, spec.y, got, spec.exp) + + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, got) + }) + } } -func TestMulExactBad(t *testing.T) { - a, err := NewDecFromString("1.000000000000000000000000000000000000123456789") - require.NoError(t, err) - b := NewDecWithPrec(1, 10) - _, err = a.MulExact(b) - require.ErrorIs(t, err, ErrUnexpectedRounding) +func TestRem(t *testing.T) { + // TO DO } -func TestQuoExactGood(t *testing.T) { - a, err := NewDecFromString("1000001") - require.NoError(t, err) - b := NewDecWithPrec(1, 6) - c, err := a.QuoExact(b) - require.NoError(t, err) - require.Equal(t, "1.000001000000000000000000000000000", c.String()) +func TestNumDecimalPlaces(t *testing.T) { + // TO DO } -func TestQuoExactBad(t *testing.T) { - a, err := NewDecFromString("1000000000000000000000000000000000000123456789") - require.NoError(t, err) - b := NewDecWithPrec(1, 10) - _, err = a.QuoExact(b) - require.ErrorIs(t, err, ErrUnexpectedRounding) +func TestCmp(t *testing.T) { + // TO DO +} + +func TestReduce(t *testing.T) { + specs := map[string]struct { + src string + exp string + decPlaces int + expErr error + }{ + "positive value": { + src: "10", + exp: "10", + decPlaces: 1, + expErr: ErrInvalidDecString, + }, + "negative value": { + src: "-10", + exp: "-10", + decPlaces: 1, + expErr: ErrInvalidDecString, + }, + "positive decimal": { + src: "1.30000", + exp: "1.3", + decPlaces: 4, + expErr: ErrInvalidDecString, + }, + "negative decimal": { + src: "-1.30000", + exp: "-1.3", + decPlaces: 4, + expErr: ErrInvalidDecString, + }, + "zero decimal and decimal places": { + src: "0.00000", + exp: "0", + decPlaces: 0, + expErr: ErrInvalidDecString, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + src, _ := NewDecFromString(spec.src) + got, gotErr := src.Reduce() + require.Equal(t, spec.exp, got.String()) + if spec.expErr != nil { + require.Equal(t, spec.decPlaces, gotErr) + } + }) + } +} + +func TestMulExact(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + exp Dec + expErr error + }{ + "200 * 200 = 200": { + x: NewDecFromInt64(200), + y: NewDecFromInt64(200), + exp: NewDecFromInt64(40000), + }, + "-200 * -200 = 40000": { + x: NewDecFromInt64(-200), + y: NewDecFromInt64(-200), + exp: NewDecFromInt64(40000), + }, + "-100 * -100 = 10000": { + x: NewDecFromInt64(-100), + y: NewDecFromInt64(-100), + exp: NewDecFromInt64(10000), + }, + "0 * 0 = 10000": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: NewDecFromInt64(0), + }, + "1.1 * 1.1 = 1.21": { + x: NewDecWithPrec(11, -1), + y: NewDecWithPrec(11, -1), + exp: NewDecWithPrec(121, -2), + }, + "1.000 * 1.000 = 1.000000": { + x: NewDecWithPrec(1000, -3), + y: NewDecWithPrec(1000, -3), + exp: NewDecWithPrec(1000000, 6), + }, + "0.0000001 * 0.0000001 = 1.21": { + x: NewDecWithPrec(00000001, -7), + y: NewDecWithPrec(00000001, -7), + exp: NewDecWithPrec(1, -14), + }, + "1.000000000000000000000000000000000000123456789 * 0.000001 = 0.000000000100000000000000000000000000000123456789": { + x: must(NewDecFromString("1.0000000000000000000000000000000000000123456789")), + y: NewDecWithPrec(1, -6), + expErr: ErrUnexpectedRounding, + }, + "1000001 * 1.000001 = 1000002.000001": { + x: NewDecFromInt64(1000001), + y: NewDecWithPrec(1000001, -6), + exp: must(NewDecFromString("1000002.000001")), + }, + "1000000000000000000000000000000000000123456789 * 100000000000 ": { + x: must(NewDecFromString("1000000000000000000000000000000000000123456789")), + y: NewDecWithPrec(1, 6), + expErr: ErrUnexpectedRounding, + }, + "1000001 * 1000000 = 1000001000000 ": { + x: NewDecFromInt64(1000001), + y: NewDecFromInt64(1000000), + exp: NewDecFromInt64(1000001000000), + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := spec.x.MulExact(spec.y) + fmt.Println(spec.x, spec.y, got, spec.exp) + + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, got) + }) + } } func TestToBigInt(t *testing.T) { @@ -458,19 +839,9 @@ func TestToSdkInt(t *testing.T) { func TestInfDecString(t *testing.T) { _, err := NewDecFromString("iNf") require.Error(t, err) - require.ErrorIs(t, err, ErrInfiniteString) + require.ErrorIs(t, err, ErrInvalidDecString) } -//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)) -//} - func must[T any](r T, err error) T { if err != nil { panic(err) From ed8ad411e079ad154169c4bcf342e09603564904 Mon Sep 17 00:00:00 2001 From: samricotta <37125168+samricotta@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:29:06 +0200 Subject: [PATCH 18/24] feat(math): Testing Sub for GDA Dec Type (#20626) --- math/dec.go | 25 +++---- math/dec_test.go | 178 +++++++++++++++++++++++++++++------------------ 2 files changed, 124 insertions(+), 79 deletions(-) diff --git a/math/dec.go b/math/dec.go index ffcc67a4d48a..19e29170243d 100644 --- a/math/dec.go +++ b/math/dec.go @@ -27,7 +27,7 @@ const ( const mathCodespace = "math" var ( - ErrInvalidDecString = errors.Register(mathCodespace, 1, "invalid decimal string") + ErrInvalidDec = errors.Register(mathCodespace, 1, "invalid decimal string") ErrUnexpectedRounding = errors.Register(mathCodespace, 2, "unexpected rounding") ErrNonIntegeral = errors.Register(mathCodespace, 3, "value is non-integral") ) @@ -49,7 +49,7 @@ type SetupConstraint func(Dec) error func AssertNotNegative() SetupConstraint { return func(d Dec) error { if d.IsNegative() { - return ErrInvalidDecString.Wrap("is negative") + return ErrInvalidDec.Wrap("is negative") } return nil } @@ -59,7 +59,7 @@ func AssertNotNegative() SetupConstraint { func AssertGreaterThanZero() SetupConstraint { return func(d Dec) error { if !d.IsPositive() { - return ErrInvalidDecString.Wrap("is negative") + return ErrInvalidDec.Wrap("is negative") } return nil } @@ -69,7 +69,7 @@ func AssertGreaterThanZero() SetupConstraint { func AssertMaxDecimals(max uint32) SetupConstraint { return func(d Dec) error { if d.NumDecimalPlaces() > max { - return ErrInvalidDecString.Wrapf("exceeds maximum decimal places: %d", max) + return ErrInvalidDec.Wrapf("exceeds maximum decimal places: %d", max) } return nil } @@ -79,14 +79,14 @@ func AssertMaxDecimals(max uint32) SetupConstraint { func NewDecFromString(s string, c ...SetupConstraint) (Dec, error) { d, _, err := apd.NewFromString(s) if err != nil { - return Dec{}, ErrInvalidDecString.Wrap(err.Error()) + return Dec{}, ErrInvalidDec.Wrap(err.Error()) } switch d.Form { case apd.NaN, apd.NaNSignaling: - return Dec{}, ErrInvalidDecString.Wrap("not a number") + return Dec{}, ErrInvalidDec.Wrap("not a number") case apd.Infinite: - return Dec{}, ErrInvalidDecString.Wrapf(s) + return Dec{}, ErrInvalidDec.Wrapf(s) default: result := Dec{*d} for _, v := range c { @@ -117,7 +117,7 @@ func (x Dec) Add(y Dec) (Dec, error) { var z Dec _, err := apd.BaseContext.Add(&z.dec, &x.dec, &y.dec) if err != nil { - return Dec{}, ErrInvalidDecString.Wrap(err.Error()) + return Dec{}, ErrInvalidDec.Wrap(err.Error()) } return z, nil @@ -128,8 +128,9 @@ func (x Dec) Add(y Dec) (Dec, error) { func (x Dec) Sub(y Dec) (Dec, error) { var z Dec _, err := apd.BaseContext.Sub(&z.dec, &x.dec, &y.dec) + if err != nil { - return Dec{}, ErrInvalidDecString + return Dec{}, ErrInvalidDec.Wrap(err.Error()) } return z, errors.Wrap(err, "decimal subtraction error") @@ -141,7 +142,7 @@ func (x Dec) Quo(y Dec) (Dec, error) { var z Dec _, err := dec128Context.Quo(&z.dec, &x.dec, &y.dec) if err != nil { - return Dec{}, ErrInvalidDecString + return Dec{}, ErrInvalidDec } return z, errors.Wrap(err, "decimal quotient error") @@ -167,7 +168,7 @@ 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, ErrInvalidDecString + return z, ErrInvalidDec } if condition.Rounded() { return z, ErrUnexpectedRounding @@ -181,7 +182,7 @@ func (x Dec) QuoInteger(y Dec) (Dec, error) { var z Dec _, err := dec128Context.QuoInteger(&z.dec, &x.dec, &y.dec) if err != nil { - return z, ErrInvalidDecString + return z, ErrInvalidDec } return z, nil } diff --git a/math/dec_test.go b/math/dec_test.go index 2cade165834f..9fcdd47cfa58 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -2,6 +2,8 @@ package math import ( "fmt" + "math" + "strconv" "strings" "testing" @@ -42,15 +44,15 @@ func TestNewDecFromString(t *testing.T) { }, "precision too high": { src: "." + strings.Repeat("9", 35), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "decimal too big": { // todo: src: strings.Repeat("9", 35), // 10^100000+10 - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "decimal too small": { src: strings.Repeat("9", 35), // -10^100000+0.99999999999999999... +1 - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "valid decimal with leading zero": { src: "01234", @@ -87,27 +89,27 @@ func TestNewDecFromString(t *testing.T) { "with setup constraint": { src: "-1", constraints: []SetupConstraint{AssertNotNegative()}, - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "empty string": { src: "", - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "NaN": { src: "NaN", - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "random string": { src: "1foo", - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "Infinity": { src: "Infinity", - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "Inf": { src: "Inf", - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, } for name, spec := range specs { @@ -142,8 +144,8 @@ func TestNewDecFromInt64(t *testing.T) { exp: "-123", }, "max value": { - src: 9223372036854775807, - exp: "9223372036854775807", + src: math.MaxInt32, + exp: strconv.Itoa(math.MaxInt32), }, "min value": { src: -9223372036854775808, @@ -238,10 +240,11 @@ func TestAdd(t *testing.T) { // x: NewDecWithPrec(1, 36), // y: NewDecWithPrec(1, 36), // constraints: []SetupConstraint{AssertMaxDecimals(34)}, - // expErr: ErrInvalidDecString, + // expErr: ErrInvalidDec, // }, // TO DO: more edge cases For example: 1^100000 + 9^100000 , 1^100000 + 1^-1 + } for name, spec := range specs { t.Run(name, func(t *testing.T) { @@ -258,96 +261,137 @@ func TestAdd(t *testing.T) { func TestSub(t *testing.T) { specs := map[string]struct { - x Dec - y Dec - exp Dec - expErr error + x Dec + y Dec + exp Dec + expErr error + constraints []SetupConstraint }{ - "zero minus zero": { - // 0 - 0 = 0 + "0 - 0 = 0": { x: NewDecFromInt64(0), y: NewDecFromInt64(0), exp: NewDecFromInt64(0), }, - "zero minus simple positive": { - // 0 - 123 = -123 + "0 - 123 = -123": { x: NewDecFromInt64(0), y: NewDecFromInt64(123), exp: NewDecFromInt64(-123), }, - "zero minus simple negative": { - // 0 - -123 = 123 + "0 - -123 = 123": { x: NewDecFromInt64(0), y: NewDecFromInt64(-123), exp: NewDecFromInt64(123), }, - "simple positive minus simple positive": { - // 123 - 123 = 0 + "123 - 123 = 0": { x: NewDecFromInt64(123), y: NewDecFromInt64(123), exp: NewDecFromInt64(0), }, - "simple negative minus simple positive": { - // -123 - 123 = 0 + "-123 - 123 = -246": { x: NewDecFromInt64(-123), y: NewDecFromInt64(123), exp: NewDecFromInt64(-246), }, - "simple negative minus simple negative": { - // -123 - -123 = 0 + "-123 - -123 = 0": { x: NewDecFromInt64(-123), y: NewDecFromInt64(-123), exp: NewDecFromInt64(0), }, - "valid decimal with decimal places add valid decimal with decimal places": { - // 1.234 - 1.234 = 0.000 + "1.234 - 1.234 = 0.000": { x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(1234, -3), exp: NewDecWithPrec(0, -3), }, - "valid decimal with decimal places minus simple positive": { - // 1.234 - 123 = -121.766 + "1.234 - 123 = -121.766": { x: NewDecWithPrec(1234, -3), y: NewDecFromInt64(123), exp: NewDecWithPrec(-121766, -3), }, - "valid decimal with decimal places minus simple negative": { - // 1.234 - -123 = 1.111 + "1.234 - -123 = 1.111": { x: NewDecWithPrec(1234, -3), y: NewDecFromInt64(-123), exp: NewDecWithPrec(124234, -3), }, - "valid decimal with decimal places minus valid negative decimal with decimal places": { - // 1.234 - -1.234 = 2.468 + "1.234 - -1.234 = 2.468": { x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(-1234, -3), exp: NewDecWithPrec(2468, -3), }, - "valid negative decimal with decimal places minus valid negative decimal with decimal places": { - // -1.234 - -1.234 = 2.468 + "-1.234 - -1.234 = 2.468": { x: NewDecWithPrec(-1234, -3), y: NewDecWithPrec(-1234, -3), exp: NewDecWithPrec(0, -3), }, - // "precision too high": { - // // 10^34 - 10^34 = 2*10^34 - // x: NewDecWithPrec(1, 36), - // y: NewDecWithPrec(1, 36), - // constraints: []SetupConstraint{AssertMaxDecimals(34)}, - // expErr: ErrInvalidDecString, - // }, + "1 - 0.999 = 0.001 - rounding after comma": { + x: NewDecFromInt64(1), + y: NewDecWithPrec(999, -3), + exp: NewDecWithPrec(1, -3), + }, + "1e100000 - 1^-1 -> Err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(1, -1), + expErr: ErrInvalidDec, + }, + "1e100000 - 1^1-> Err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(1, -1), + expErr: ErrInvalidDec, + }, + "upper exp limit exceeded": { + x: NewDecWithPrec(1, 100_001), + y: NewDecWithPrec(1, 100_001), + expErr: ErrInvalidDec, + }, + "lower exp limit exceeded": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(1, -100_001), + expErr: ErrInvalidDec, + }, + "1e100000 - 1 = 999..9": { + x: NewDecWithPrec(1, 100_000), + y: NewDecFromInt64(1), + exp: must(NewDecFromString(strings.Repeat("9", 100_000))), + }, + "1e100000 - 0 = 1e100000": { + x: NewDecWithPrec(1, 100_000), + y: NewDecFromInt64(0), + exp: must(NewDecFromString("1e100000")), + }, + "1e100001 - 0 -> err": { + x: NewDecWithPrec(1, 100_001), + y: NewDecFromInt64(0), + expErr: ErrInvalidDec, + }, + "1e100000 - -1 -> 100..1": { + x: NewDecWithPrec(1, 100_000), + y: NewDecFromInt64(-1), + exp: must(NewDecFromString("1" + strings.Repeat("0", 99_999) + "1")), + }, + "1e-100000 - 0 = 1e-100000": { + x: NewDecWithPrec(1, -100_000), + y: NewDecFromInt64(0), + exp: must(NewDecFromString("1e-100000")), + }, + "1e-100001 - 0 -> err": { + x: NewDecWithPrec(1, -100_001), + y: NewDecFromInt64(0), + expErr: ErrInvalidDec, + }, + "1e-100000 - -1 -> 0.000..01": { + x: NewDecWithPrec(1, -100_000), + y: NewDecFromInt64(-1), + exp: must(NewDecFromString("1." + strings.Repeat("0", 99999) + "1")), + }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { got, gotErr := spec.x.Sub(spec.y) - fmt.Println(got) if spec.expErr != nil { - require.ErrorIs(t, gotErr, spec.expErr, got) + require.ErrorIs(t, gotErr, spec.expErr) return } - require.NoError(t, gotErr) - assert.Equal(t, spec.exp, got) + assert.True(t, spec.exp.Equal(got)) }) } } @@ -363,7 +407,7 @@ func TestQuo(t *testing.T) { "0 / 0": { x: NewDecFromInt64(0), y: NewDecFromInt64(0), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, " 0 / 123 = 0": { x: NewDecFromInt64(0), @@ -379,12 +423,12 @@ func TestQuo(t *testing.T) { "123 / 0 = 0": { x: NewDecFromInt64(123), y: NewDecFromInt64(0), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "-123 / 0 = 0": { x: NewDecFromInt64(-123), y: NewDecFromInt64(0), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "123 / 123 = 1": { // the answer is showing up as 0 although it should be 1. Again with 34 precision it is mismatched @@ -432,7 +476,7 @@ func TestQuo(t *testing.T) { // x: NewDecWithPrec(1, 36), // y: NewDecWithPrec(1, 36), // constraints: []SetupConstraint{AssertMaxDecimals(34)}, - // expErr: ErrInvalidDecString, + // expErr: ErrInvalidDec, // }, } for name, spec := range specs { @@ -461,7 +505,7 @@ func TestQuoExact(t *testing.T) { "0 / 0": { x: NewDecFromInt64(0), y: NewDecFromInt64(0), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, " 0 / 123 = 0": { x: NewDecFromInt64(0), @@ -477,12 +521,12 @@ func TestQuoExact(t *testing.T) { "123 / 0 = 0": { x: NewDecFromInt64(123), y: NewDecFromInt64(0), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "-123 / 0 = 0": { x: NewDecFromInt64(-123), y: NewDecFromInt64(0), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "123 / 123 = 1": { // the answer is showing up as 0 although it should be 1. Again with 34 precision it is mismatched @@ -530,7 +574,7 @@ func TestQuoExact(t *testing.T) { // x: NewDecWithPrec(1, 36), // y: NewDecWithPrec(1, 36), // constraints: []SetupConstraint{AssertMaxDecimals(34)}, - // expErr: ErrInvalidDecString, + // expErr: ErrInvalidDec, // }, } for name, spec := range specs { @@ -559,7 +603,7 @@ func TestQuoInteger(t *testing.T) { "0 / 0": { x: NewDecFromInt64(0), y: NewDecFromInt64(0), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, " 0 / 123 = 0": { x: NewDecFromInt64(0), @@ -575,12 +619,12 @@ func TestQuoInteger(t *testing.T) { "123 / 0 = 0": { x: NewDecFromInt64(123), y: NewDecFromInt64(0), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "-123 / 0 = 0": { x: NewDecFromInt64(-123), y: NewDecFromInt64(0), - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "123 / 123 = 1": { x: NewDecFromInt64(123), @@ -628,7 +672,7 @@ func TestQuoInteger(t *testing.T) { // x: NewDecWithPrec(1, 36), // y: NewDecWithPrec(1, 36), // constraints: []SetupConstraint{AssertMaxDecimals(34)}, - // expErr: ErrInvalidDecString, + // expErr: ErrInvalidDec, // }, } for name, spec := range specs { @@ -669,31 +713,31 @@ func TestReduce(t *testing.T) { src: "10", exp: "10", decPlaces: 1, - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "negative value": { src: "-10", exp: "-10", decPlaces: 1, - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "positive decimal": { src: "1.30000", exp: "1.3", decPlaces: 4, - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "negative decimal": { src: "-1.30000", exp: "-1.3", decPlaces: 4, - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, "zero decimal and decimal places": { src: "0.00000", exp: "0", decPlaces: 0, - expErr: ErrInvalidDecString, + expErr: ErrInvalidDec, }, } for name, spec := range specs { @@ -839,7 +883,7 @@ func TestToSdkInt(t *testing.T) { func TestInfDecString(t *testing.T) { _, err := NewDecFromString("iNf") require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidDecString) + require.ErrorIs(t, err, ErrInvalidDec) } func must[T any](r T, err error) T { From 5da8fc4a5950fd553a6887320b714de5bd78e3e8 Mon Sep 17 00:00:00 2001 From: samricotta <37125168+samricotta@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:10:17 +0200 Subject: [PATCH 19/24] feat(math): Upstream GDA based decimal type - Add function coverage (#20726) --- math/dec_test.go | 81 ++++++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/math/dec_test.go b/math/dec_test.go index 9fcdd47cfa58..b95954d27af9 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -168,83 +168,96 @@ func TestAdd(t *testing.T) { exp Dec expErr error }{ - "zero add zero": { - // 0 + 0 = 0 + "0 + 0 = 0": { x: NewDecFromInt64(0), y: NewDecFromInt64(0), exp: NewDecFromInt64(0), }, - "zero add simple positive": { - // 0 + 123 = 123 + "0 + 123 = 123": { x: NewDecFromInt64(0), y: NewDecFromInt64(123), exp: NewDecFromInt64(123), }, - "zero and simple negative": { - // 0 + -123 = -123 + "0 + -123 = -123": { x: NewDecFromInt64(0), y: NewDecFromInt64(-123), exp: NewDecFromInt64(-123), }, - "simple positive add simple positive": { - // 123 + 123 = 246 + "123 + 123 = 246": { x: NewDecFromInt64(123), y: NewDecFromInt64(123), exp: NewDecFromInt64(246), }, - "simple negative add simple positive": { - // -123 + 123 = 0 + "-123 + 123 = 0": { x: NewDecFromInt64(-123), y: NewDecFromInt64(123), exp: NewDecFromInt64(0), }, - "simple negative add simple negative": { - // -123 + -123 = -246 + "-123 + -123 = -246": { x: NewDecFromInt64(-123), y: NewDecFromInt64(-123), exp: NewDecFromInt64(-246), }, - "valid decimal with decimal places add valid decimal with decimal places": { - // 1.234 + 1.234 = 2.468 + "1.234 + 1.234 = 2.468": { x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(1234, -3), exp: NewDecWithPrec(2468, -3), }, - "valid decimal with decimal places and simple positive": { - // 1.234 + 123 = 124.234 + "1.234 + 123 = 124.234": { x: NewDecWithPrec(1234, -3), y: NewDecFromInt64(123), exp: NewDecWithPrec(124234, -3), }, - "valid decimal with decimal places and simple negative": { - // 1.234 + -123 = 1.111 + "1.234 + -123 = -121.766": { x: NewDecWithPrec(1234, -3), y: NewDecFromInt64(-123), - exp: NewDecWithPrec(111, -3), + exp: must(NewDecFromString("-121.766")), }, - - "valid decimal with decimal places add valid negative decimal with decimal places": { - // 1.234 + -1.234 = 0 + "1.234 + -1.234 = 0": { x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(-1234, -3), exp: NewDecWithPrec(0, -3), }, - "valid negative decimal with decimal places add valid negative decimal with decimal places": { - // -1.234 + -1.234 = -2.468 + "-1.234 + -1.234 = -2.468": { x: NewDecWithPrec(-1234, -3), y: NewDecWithPrec(-1234, -3), exp: NewDecWithPrec(-2468, -3), }, - // "precision too high": { - // // 10^34 + 10^34 = 2*10^34 - // x: NewDecWithPrec(1, 36), - // y: NewDecWithPrec(1, 36), - // constraints: []SetupConstraint{AssertMaxDecimals(34)}, - // expErr: ErrInvalidDec, - // }, - - // TO DO: more edge cases For example: 1^100000 + 9^100000 , 1^100000 + 1^-1 - + "1e100000 + 9e900000 -> Err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(9, 900_000), + expErr: ErrInvalidDec, + }, + "1e100000 + -9e900000 -> Err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(9, 900_000), + expErr: ErrInvalidDec, + }, + "1e100000 + 1e^-1 -> err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(1, -1), + expErr: ErrInvalidDec, + }, + "1e100000 + -1e^-1 -> err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(-1, -1), + expErr: ErrInvalidDec, + }, + "1e100000 + 1 -> 100..1": { + x: NewDecWithPrec(1, 100_000), + y: NewDecFromInt64(1), + exp: must(NewDecWithPrec(1, 100_000).Add(NewDecFromInt64(1))), + }, + "1e100000 + 0 -> err": { + x: NewDecWithPrec(1, 100_001), + y: NewDecFromInt64(0), + expErr: ErrInvalidDec, + }, + "-1e100000 + 0 -> err": { + x: NewDecWithPrec(-1, 100_001), + y: NewDecFromInt64(0), + expErr: ErrInvalidDec, + }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { From cc09fe849e78e072aa6430e477dc429feddabded Mon Sep 17 00:00:00 2001 From: samricotta <37125168+samricotta@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:38:37 +0200 Subject: [PATCH 20/24] feat(math): Upstream GDA based decimal type testing (#20763) --- math/dec.go | 9 +- math/dec_test.go | 578 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 487 insertions(+), 100 deletions(-) diff --git a/math/dec.go b/math/dec.go index 19e29170243d..de76c65126dc 100644 --- a/math/dec.go +++ b/math/dec.go @@ -154,7 +154,7 @@ 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 z, ErrInvalidDec } if condition.Rounded() { return z, ErrUnexpectedRounding @@ -187,11 +187,14 @@ func (x Dec) QuoInteger(y Dec) (Dec, error) { return z, nil } -// Rem returns the integral remainder from `x/y` (formatted as decimal128, with 34 digit precision) without +// Modulo 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) { +func (x Dec) Modulo(y Dec) (Dec, error) { var z Dec _, err := dec128Context.Rem(&z.dec, &x.dec, &y.dec) + if err != nil { + return z, ErrInvalidDec + } return z, errors.Wrap(err, "decimal remainder error") } diff --git a/math/dec_test.go b/math/dec_test.go index b95954d27af9..bc659383de15 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -39,19 +39,15 @@ func TestNewDecFromString(t *testing.T) { exp: must(NewDecWithPrec(-1, 34).Add(NewDecFromInt64(1))), }, "max decimal": { - // todo: src: strings.Repeat("9", 34), + src: strings.Repeat("9", 34), exp: must(NewDecWithPrec(1, 34).Sub(NewDecFromInt64(1))), }, - "precision too high": { - src: "." + strings.Repeat("9", 35), + "too big": { + src: strings.Repeat("9", 100_0000), expErr: ErrInvalidDec, }, - "decimal too big": { - // todo: src: strings.Repeat("9", 35), // 10^100000+10 - expErr: ErrInvalidDec, - }, - "decimal too small": { - src: strings.Repeat("9", 35), // -10^100000+0.99999999999999999... +1 + "too small": { + src: "-" + strings.Repeat("9", 100_0000), expErr: ErrInvalidDec, }, "valid decimal with leading zero": { @@ -86,11 +82,6 @@ func TestNewDecFromString(t *testing.T) { src: "-1.23e4", exp: NewDecWithPrec(-123, 2), }, - "with setup constraint": { - src: "-1", - constraints: []SetupConstraint{AssertNotNegative()}, - expErr: ErrInvalidDec, - }, "empty string": { src: "", expErr: ErrInvalidDec, @@ -120,7 +111,7 @@ func TestNewDecFromString(t *testing.T) { return } require.NoError(t, gotErr) - assert.Equal(t, spec.exp.String(), got.String()) + assert.True(t, spec.exp.Equal(got)) }) } } @@ -320,7 +311,7 @@ func TestSub(t *testing.T) { y: NewDecFromInt64(123), exp: NewDecWithPrec(-121766, -3), }, - "1.234 - -123 = 1.111": { + "1.234 - -123 = 124.234": { x: NewDecWithPrec(1234, -3), y: NewDecFromInt64(-123), exp: NewDecWithPrec(124234, -3), @@ -367,7 +358,7 @@ func TestSub(t *testing.T) { }, "1e100000 - 0 = 1e100000": { x: NewDecWithPrec(1, 100_000), - y: NewDecFromInt64(0), + y: NewDecFromInt64(0111), exp: must(NewDecFromString("1e100000")), }, "1e100001 - 0 -> err": { @@ -417,7 +408,7 @@ func TestQuo(t *testing.T) { exp Dec expErr error }{ - "0 / 0": { + "0 / 0 -> Err": { x: NewDecFromInt64(0), y: NewDecFromInt64(0), expErr: ErrInvalidDec, @@ -427,12 +418,6 @@ func TestQuo(t *testing.T) { y: NewDecFromInt64(123), exp: NewDecFromInt64(0), }, - // answer is -0 but this is not throwing an error - // "0 / -123 = 0": { - // x: NewDecFromInt64(0), - // y: NewDecFromInt64(-123), - // exp: NewDecFromInt64(-0), - // }, "123 / 0 = 0": { x: NewDecFromInt64(123), y: NewDecFromInt64(0), @@ -444,12 +429,11 @@ func TestQuo(t *testing.T) { expErr: ErrInvalidDec, }, "123 / 123 = 1": { - // the answer is showing up as 0 although it should be 1. Again with 34 precision it is mismatched x: NewDecFromInt64(123), y: NewDecFromInt64(123), exp: must(NewDecFromString("1.000000000000000000000000000000000")), }, - "-123 / 123 = 1": { + "-123 / 123 = -1": { x: NewDecFromInt64(-123), y: NewDecFromInt64(123), exp: must(NewDecFromString("-1.000000000000000000000000000000000")), @@ -459,22 +443,22 @@ func TestQuo(t *testing.T) { y: NewDecFromInt64(-123), exp: must(NewDecFromString("1.000000000000000000000000000000000")), }, - "1.234 / 1.234": { + "1.234 / 1.234 = 1": { x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(1234, -3), exp: must(NewDecFromString("1.000000000000000000000000000000000")), }, - "-1.234 / 1234 = -121.766": { + "-1.234 / 1234 = -1": { x: NewDecWithPrec(-1234, -3), y: NewDecWithPrec(1234, -3), exp: must(NewDecFromString("-1.000000000000000000000000000000000")), }, - "1.234 / -123 = 1.111": { + "1.234 / -123 = 1.0100": { x: NewDecWithPrec(1234, -3), y: NewDecFromInt64(-123), exp: must(NewDecFromString("-0.01003252032520325203252032520325203")), }, - "1.234 / -1.234 = 2.468": { + "1.234 / -1.234 = -1": { x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(-1234, -3), exp: must(NewDecFromString("-1.000000000000000000000000000000000")), @@ -484,25 +468,130 @@ func TestQuo(t *testing.T) { y: NewDecWithPrec(-1234, -3), exp: must(NewDecFromString("1.000000000000000000000000000000000")), }, - // "precision too high": { - // // 10^34 / 10^34 = 2*10^34 - // x: NewDecWithPrec(1, 36), - // y: NewDecWithPrec(1, 36), - // constraints: []SetupConstraint{AssertMaxDecimals(34)}, - // expErr: ErrInvalidDec, - // }, + "3 / -9 = -0.3333...3 - round down": { + x: NewDecFromInt64(3), + y: NewDecFromInt64(-9), + exp: must(NewDecFromString("-0.3333333333333333333333333333333333")), + }, + "4 / 9 = 0.4444...4 - round down": { + x: NewDecFromInt64(4), + y: NewDecFromInt64(9), + exp: must(NewDecFromString("0.4444444444444444444444444444444444")), + }, + "5 / 9 = 0.5555...6 - round up": { + x: NewDecFromInt64(5), + y: NewDecFromInt64(9), + exp: must(NewDecFromString("0.5555555555555555555555555555555556")), + }, + "6 / 9 = 0.6666...7 - round up": { + x: NewDecFromInt64(6), + y: NewDecFromInt64(9), + exp: must(NewDecFromString("0.6666666666666666666666666666666667")), + }, + "7 / 9 = 0.7777...8 - round up": { + x: NewDecFromInt64(7), + y: NewDecFromInt64(9), + exp: must(NewDecFromString("0.7777777777777777777777777777777778")), + }, + "8 / 9 = 0.8888...9 - round up": { + x: NewDecFromInt64(8), + y: NewDecFromInt64(9), + exp: must(NewDecFromString("0.8888888888888888888888888888888889")), + }, + "9e-34 / 10 = 9e-35 - no rounding": { + x: NewDecWithPrec(9, -34), + y: NewDecFromInt64(10), + exp: must(NewDecFromString("9e-35")), + }, + "9e-35 / 10 = 9e-36 - no rounding": { + x: NewDecWithPrec(9, -35), + y: NewDecFromInt64(10), + exp: must(NewDecFromString("9e-36")), + }, + "high precision - min/0.1": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(1, -1), + exp: NewDecWithPrec(1, -99_999), + }, + "high precision - min/1": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(1, 0), + exp: NewDecWithPrec(1, -100_000), + }, + "high precision - min/10": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(1, 1), + expErr: ErrInvalidDec, + }, + "high precision - <_min/0.1": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(1, -1), + exp: NewDecWithPrec(1, -100_000), + }, + "high precision - <_min/1": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(1, 0), + expErr: ErrInvalidDec, + }, + "high precision - <_min/10": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(1, 1), + expErr: ErrInvalidDec, + }, + "high precision - min/-0.1": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(-1, -1), + exp: NewDecWithPrec(-1, -99_999), + }, + "high precision - min/-1": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(-1, 0), + exp: NewDecWithPrec(-1, -100_000), + }, + "high precision - min/-10": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(-1, 1), + expErr: ErrInvalidDec, + }, + "high precision - <_min/-0.1": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(-1, -1), + exp: NewDecWithPrec(-1, -100_000), + }, + "high precision - <_min/-1": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(-1, 0), + expErr: ErrInvalidDec, + }, + "high precision - <_min/-10": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(-1, 1), + expErr: ErrInvalidDec, + }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { got, gotErr := spec.x.Quo(spec.y) - fmt.Println(spec.x, spec.y, got, spec.exp) + if name == "1.234 / -123 = 1.111" { + fmt.Println("got", got) + } if spec.expErr != nil { require.ErrorIs(t, gotErr, spec.expErr) return } require.NoError(t, gotErr) - assert.Equal(t, spec.exp, got) + last35 := func(s string) string { + var x int + if len(s) < 36 { + x = 0 + } else { + x = len(s) - 36 + } + return fmt.Sprintf("%s(%d)", s[x:], len(s)) + } + gotReduced, _ := got.Reduce() + assert.True(t, spec.exp.Equal(gotReduced), "exp %s, got: %s", last35(spec.exp.String()), last35(gotReduced.String())) }) } } @@ -515,7 +604,7 @@ func TestQuoExact(t *testing.T) { exp Dec expErr error }{ - "0 / 0": { + "0 / 0 -> Err": { x: NewDecFromInt64(0), y: NewDecFromInt64(0), expErr: ErrInvalidDec, @@ -525,24 +614,17 @@ func TestQuoExact(t *testing.T) { y: NewDecFromInt64(123), exp: NewDecFromInt64(0), }, - // answer is -0 but this is not throwing an error - // "0 / -123 = 0": { - // x: NewDecFromInt64(0), - // y: NewDecFromInt64(-123), - // exp: NewDecFromInt64(-0), - // }, - "123 / 0 = 0": { + "123 / 0 -> Err": { x: NewDecFromInt64(123), y: NewDecFromInt64(0), expErr: ErrInvalidDec, }, - "-123 / 0 = 0": { + "-123 / 0 -> Err": { x: NewDecFromInt64(-123), y: NewDecFromInt64(0), expErr: ErrInvalidDec, }, "123 / 123 = 1": { - // the answer is showing up as 0 although it should be 1. Again with 34 precision it is mismatched x: NewDecFromInt64(123), y: NewDecFromInt64(123), exp: must(NewDecFromString("1.000000000000000000000000000000000")), @@ -557,7 +639,7 @@ func TestQuoExact(t *testing.T) { y: NewDecFromInt64(-123), exp: must(NewDecFromString("1.000000000000000000000000000000000")), }, - "1.234 / 1.234": { + "1.234 / 1.234 = 1": { x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(1234, -3), exp: must(NewDecFromString("1.000000000000000000000000000000000")), @@ -567,12 +649,12 @@ func TestQuoExact(t *testing.T) { y: NewDecWithPrec(1234, -3), exp: must(NewDecFromString("-1.000000000000000000000000000000000")), }, - "1.234 / -123 = 1.111": { + "1.234 / -123 -> Err": { x: NewDecWithPrec(1234, -3), y: NewDecFromInt64(-123), expErr: ErrUnexpectedRounding, }, - "1.234 / -1.234 = 2.468": { + "1.234 / -1.234 = -1": { x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(-1234, -3), exp: must(NewDecFromString("-1.000000000000000000000000000000000")), @@ -582,25 +664,117 @@ func TestQuoExact(t *testing.T) { y: NewDecWithPrec(-1234, -3), exp: must(NewDecFromString("1.000000000000000000000000000000000")), }, - // "precision too high": { - // // 10^34 / 10^34 = 2*10^34 - // x: NewDecWithPrec(1, 36), - // y: NewDecWithPrec(1, 36), - // constraints: []SetupConstraint{AssertMaxDecimals(34)}, - // expErr: ErrInvalidDec, - // }, + "3 / -9 -> Err": { + x: NewDecFromInt64(3), + y: NewDecFromInt64(-9), + expErr: ErrUnexpectedRounding, + }, + "4 / 9 -> Err": { + x: NewDecFromInt64(4), + y: NewDecFromInt64(9), + expErr: ErrUnexpectedRounding, + }, + "5 / 9 -> Err": { + x: NewDecFromInt64(5), + y: NewDecFromInt64(9), + expErr: ErrUnexpectedRounding, + }, + "6 / 9 -> Err": { + x: NewDecFromInt64(6), + y: NewDecFromInt64(9), + expErr: ErrUnexpectedRounding, + }, + "7 / 9 -> Err": { + x: NewDecFromInt64(7), + y: NewDecFromInt64(9), + expErr: ErrUnexpectedRounding, + }, + "8 / 9 -> Err": { + x: NewDecFromInt64(8), + y: NewDecFromInt64(9), + expErr: ErrUnexpectedRounding, + }, + "9e-34 / 10 = 9e-35 - no rounding": { + x: NewDecWithPrec(9, -34), + y: NewDecFromInt64(10), + exp: must(NewDecFromString("0.00000000000000000000000000000000009000000000000000000000000000000000")), + }, + "9e-35 / 10 = 9e-36 - no rounding": { + x: NewDecWithPrec(9, -35), + y: NewDecFromInt64(10), + exp: must(NewDecFromString("9e-36")), + }, + "high precision - min/0.1": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(1, -1), + exp: NewDecWithPrec(1, -99_999), + }, + "high precision - min/1": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(1, 0), + exp: NewDecWithPrec(1, -100_000), + }, + "high precision - min/10": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(1, 1), + expErr: ErrInvalidDec, + }, + "high precision - <_min/0.1": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(1, -1), + exp: NewDecWithPrec(1, -100_000), + }, + "high precision - <_min/1": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(1, 0), + expErr: ErrInvalidDec, + }, + "high precision - <_min/10 -> Err": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(1, 1), + expErr: ErrInvalidDec, + }, + "high precision - min/-0.1": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(-1, -1), + exp: NewDecWithPrec(-1, -99_999), + }, + "high precision - min/-1": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(-1, 0), + exp: NewDecWithPrec(-1, -100_000), + }, + "high precision - min/-10 -> Err": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(-1, 1), + expErr: ErrInvalidDec, + }, + "high precision - <_min/-0.1": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(-1, -1), + exp: NewDecWithPrec(-1, -100_000), + }, + "high precision - <_min/-1": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(-1, 0), + expErr: ErrInvalidDec, + }, + "high precision - <_min/-10": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(-1, 1), + expErr: ErrInvalidDec, + }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { - got, gotErr := spec.x.Quo(spec.y) - fmt.Println(spec.x, spec.y, got, spec.exp) + got, gotErr := spec.x.QuoExact(spec.y) if spec.expErr != nil { require.ErrorIs(t, gotErr, spec.expErr) return } require.NoError(t, gotErr) - assert.Equal(t, spec.exp, got) + assert.True(t, spec.exp.Equal(got)) }) } } @@ -613,7 +787,7 @@ func TestQuoInteger(t *testing.T) { exp Dec expErr error }{ - "0 / 0": { + "0 / 0 -> Err": { x: NewDecFromInt64(0), y: NewDecFromInt64(0), expErr: ErrInvalidDec, @@ -623,18 +797,12 @@ func TestQuoInteger(t *testing.T) { y: NewDecFromInt64(123), exp: NewDecFromInt64(0), }, - // answer is -0 but this is not throwing an error - // "0 / -123 = 0": { - // x: NewDecFromInt64(0), - // y: NewDecFromInt64(-123), - // exp: NewDecFromInt64(-0), - // }, - "123 / 0 = 0": { + "123 / 0 -> Err": { x: NewDecFromInt64(123), y: NewDecFromInt64(0), expErr: ErrInvalidDec, }, - "-123 / 0 = 0": { + "-123 / -> Err": { x: NewDecFromInt64(-123), y: NewDecFromInt64(0), expErr: ErrInvalidDec, @@ -642,7 +810,7 @@ func TestQuoInteger(t *testing.T) { "123 / 123 = 1": { x: NewDecFromInt64(123), y: NewDecFromInt64(123), - exp: NewDecFromInt64(1), + exp: NewDecFromInt64(5), }, "-123 / 123 = -1": { x: NewDecFromInt64(-123), @@ -664,12 +832,6 @@ func TestQuoInteger(t *testing.T) { y: NewDecWithPrec(1234, -3), exp: NewDecFromInt64(-1), }, - // "1.234 / -123 = -0": { - //-0 - // x: NewDecWithPrec(1234, -3), - // y: NewDecFromInt64(-123), - // exp: NewDecFromInt64(1), - // }, "1.234 / -1.234 = 2.468": { x: NewDecWithPrec(1234, -3), y: NewDecWithPrec(-1234, -3), @@ -680,39 +842,228 @@ func TestQuoInteger(t *testing.T) { y: NewDecWithPrec(-1234, -3), exp: NewDecFromInt64(1), }, - // "precision too high": { - // // 10^34 / 10^34 = 2*10^34 - // x: NewDecWithPrec(1, 36), - // y: NewDecWithPrec(1, 36), - // constraints: []SetupConstraint{AssertMaxDecimals(34)}, - // expErr: ErrInvalidDec, - // }, + "3 / -9 = 0": { + x: NewDecFromInt64(3), + y: NewDecFromInt64(-9), + exp: must(NewDecFromString("0")), + }, + "8 / 9 = 0": { + x: NewDecFromInt64(8), + y: NewDecFromInt64(9), + exp: must(NewDecFromString("0")), + }, + "high precision - min/0.1": { + x: NewDecWithPrec(1, -100_000), + y: NewDecWithPrec(1, -1), + exp: NewDecFromInt64(0), + }, + "high precision - <_min/-1 -> Err": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(-1, 0), + expErr: ErrInvalidDec, + }, + "high precision - <_min/-10 -> Err": { + x: NewDecWithPrec(1, -100_001), + y: NewDecWithPrec(-1, 1), + expErr: ErrInvalidDec, + }, + "1e000 / 1 -> Err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecFromInt64(1), + expErr: ErrInvalidDec, + }, + "1e100000 - 1^1 -> Err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(1, -1), + expErr: ErrInvalidDec, + }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { got, gotErr := spec.x.QuoInteger(spec.y) - fmt.Println(spec.x, spec.y, got, spec.exp) if spec.expErr != nil { require.ErrorIs(t, gotErr, spec.expErr) return } require.NoError(t, gotErr) - assert.Equal(t, spec.exp, got) + assert.True(t, spec.exp.Equal(got)) }) } } -func TestRem(t *testing.T) { - // TO DO +func TestModulo(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + exp Dec + expErr error + }{ + "0 / 123 = 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(0), + }, + "123 / 10 = 3": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(10), + exp: NewDecFromInt64(3), + }, + "123 / -10 = 3": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(-10), + exp: NewDecFromInt64(3), + }, + "-123 / 10 = -3": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(10), + exp: NewDecFromInt64(-3), + }, + "1.234 / 1 = 0.234": { + x: NewDecWithPrec(1234, -3), + y: NewDecFromInt64(1), + exp: NewDecWithPrec(234, -3), + }, + "1.234 / 0.1 = 0.034": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(1, -1), + exp: NewDecWithPrec(34, -3), + }, + "1.234 / 1.1 = 0.134": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(11, -1), + exp: NewDecWithPrec(134, -3), + }, + "10 / 0 -> Err": { + x: NewDecFromInt64(10), + y: NewDecFromInt64(0), + expErr: ErrInvalidDec, + }, + "-1e0000 / 9e0000 = 1e0000": { + x: NewDecWithPrec(-1, 100_000), + y: NewDecWithPrec(9, 100_000), + exp: NewDecWithPrec(-1, 100_000), + }, + "1e0000 / 9e0000 = 1e0000": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(9, 100_000), + exp: NewDecWithPrec(1, 100_000), + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := spec.x.Modulo(spec.y) + + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.True(t, spec.exp.Equal(got)) + }) + } } func TestNumDecimalPlaces(t *testing.T) { - // TO DO + specs := map[string]struct { + src Dec + exp uint32 + }{ + "integer": { + src: NewDecFromInt64(123), + exp: 0, + }, + "one decimal place": { + src: NewDecWithPrec(1234, -1), + exp: 1, + }, + "two decimal places": { + src: NewDecWithPrec(12345, -2), + exp: 2, + }, + "three decimal places": { + src: NewDecWithPrec(123456, -3), + exp: 3, + }, + "trailing zeros": { + src: NewDecWithPrec(123400, -4), + exp: 4, + }, + "zero value": { + src: NewDecFromInt64(0), + exp: 0, + }, + "negative value": { + src: NewDecWithPrec(-12345, -3), + exp: 3, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := spec.src.NumDecimalPlaces() + assert.Equal(t, spec.exp, got) + }) + } } func TestCmp(t *testing.T) { - // TO DO + specs := map[string]struct { + x Dec + y Dec + exp int + }{ + "0 == 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: 0, + }, + "0 < 123 = -1": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: -1, + }, + "123 > 0 = 1": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + exp: 1, + }, + "-123 < 0 = -1": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + exp: -1, + }, + "123 == 123": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: 0, + }, + "-123 == -123": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: 0, + }, + "1.234 == 1.234": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(1234, -3), + exp: 0, + }, + "1.234 > 1.233": { + x: NewDecWithPrec(1234, -3), + y: NewDecWithPrec(1233, -3), + exp: 1, + }, + "1.233 < 1.234": { + x: NewDecWithPrec(1233, -3), + y: NewDecWithPrec(1234, -3), + exp: -1, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := spec.x.Cmp(spec.y) + assert.Equal(t, spec.exp, got) + }) + } } func TestReduce(t *testing.T) { @@ -772,7 +1123,7 @@ func TestMulExact(t *testing.T) { exp Dec expErr error }{ - "200 * 200 = 200": { + "200 * 200 = 40000": { x: NewDecFromInt64(200), y: NewDecFromInt64(200), exp: NewDecFromInt64(40000), @@ -787,7 +1138,7 @@ func TestMulExact(t *testing.T) { y: NewDecFromInt64(-100), exp: NewDecFromInt64(10000), }, - "0 * 0 = 10000": { + "0 * 0 = 0": { x: NewDecFromInt64(0), y: NewDecFromInt64(0), exp: NewDecFromInt64(0), @@ -800,9 +1151,9 @@ func TestMulExact(t *testing.T) { "1.000 * 1.000 = 1.000000": { x: NewDecWithPrec(1000, -3), y: NewDecWithPrec(1000, -3), - exp: NewDecWithPrec(1000000, 6), + exp: must(NewDecFromString("1.000000")), }, - "0.0000001 * 0.0000001 = 1.21": { + "0.0000001 * 0.0000001 = 0": { x: NewDecWithPrec(00000001, -7), y: NewDecWithPrec(00000001, -7), exp: NewDecWithPrec(1, -14), @@ -827,12 +1178,45 @@ func TestMulExact(t *testing.T) { y: NewDecFromInt64(1000000), exp: NewDecFromInt64(1000001000000), }, + "1e0000 * 1e0000 -> Err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(1, 100_000), + expErr: ErrInvalidDec, + }, + "1e0000 * 1 = 1e0000": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(1, 0), + exp: NewDecWithPrec(1, 100_000), + }, + "1e100000 * 9 = 9e100000": { + x: NewDecWithPrec(1, 100_000), + y: NewDecFromInt64(9), + exp: NewDecWithPrec(9, 100_000), + }, + "1e100000 * 10 = err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(1, 1), + expErr: ErrInvalidDec, + }, + "1e0000 * -1 = 1e0000": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(-1, 0), + exp: NewDecWithPrec(1, 100_000), + }, + "1e100000 * -9 = 9e100000": { + x: NewDecWithPrec(1, 100_000), + y: NewDecFromInt64(-9), + exp: NewDecWithPrec(9, 100_000), + }, + "1e100000 * -10 = err": { + x: NewDecWithPrec(1, 100_000), + y: NewDecWithPrec(-1, 1), + expErr: ErrInvalidDec, + }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { got, gotErr := spec.x.MulExact(spec.y) - fmt.Println(spec.x, spec.y, got, spec.exp) - if spec.expErr != nil { require.ErrorIs(t, gotErr, spec.expErr) return From b45aec2ad7ca999764fbefd4d0000f16cfb4e6aa Mon Sep 17 00:00:00 2001 From: samricotta <37125168+samricotta@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:39:29 +0200 Subject: [PATCH 21/24] feat(math): Upstream GDA based decimal type - errors (#20827) --- math/dec.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/math/dec.go b/math/dec.go index de76c65126dc..354d5b870f81 100644 --- a/math/dec.go +++ b/math/dec.go @@ -142,7 +142,7 @@ func (x Dec) Quo(y Dec) (Dec, error) { var z Dec _, err := dec128Context.Quo(&z.dec, &x.dec, &y.dec) if err != nil { - return Dec{}, ErrInvalidDec + return Dec{}, ErrInvalidDec.Wrap(err.Error()) } return z, errors.Wrap(err, "decimal quotient error") @@ -168,7 +168,7 @@ 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, ErrInvalidDec + return z, ErrInvalidDec.Wrap(err.Error()) } if condition.Rounded() { return z, ErrUnexpectedRounding @@ -182,7 +182,7 @@ func (x Dec) QuoInteger(y Dec) (Dec, error) { var z Dec _, err := dec128Context.QuoInteger(&z.dec, &x.dec, &y.dec) if err != nil { - return z, ErrInvalidDec + return z, ErrInvalidDec.Wrap(err.Error()) } return z, nil } From 777b298f96dcb1af4c1195d29f1edaf5e7a74c34 Mon Sep 17 00:00:00 2001 From: Alexander Peters Date: Wed, 3 Jul 2024 17:07:58 +0200 Subject: [PATCH 22/24] Fix tests and minor refactoring (#20861) --- math/dec.go | 41 ++--------------------------------------- math/dec_rapid_test.go | 2 +- math/dec_test.go | 12 ++++++------ 3 files changed, 9 insertions(+), 46 deletions(-) diff --git a/math/dec.go b/math/dec.go index 354d5b870f81..d355c6b1c4fa 100644 --- a/math/dec.go +++ b/math/dec.go @@ -43,40 +43,8 @@ var dec128Context = apd.Context{ Traps: apd.DefaultTraps, } -type SetupConstraint func(Dec) error - -// AssertNotNegative greater or equal 0 -func AssertNotNegative() SetupConstraint { - return func(d Dec) error { - if d.IsNegative() { - return ErrInvalidDec.Wrap("is negative") - } - return nil - } -} - -// AssertGreaterThanZero greater than 0 -func AssertGreaterThanZero() SetupConstraint { - return func(d Dec) error { - if !d.IsPositive() { - return ErrInvalidDec.Wrap("is negative") - } - return nil - } -} - -// AssertMaxDecimals limit the decimal places -func AssertMaxDecimals(max uint32) SetupConstraint { - return func(d Dec) error { - if d.NumDecimalPlaces() > max { - return ErrInvalidDec.Wrapf("exceeds maximum decimal places: %d", max) - } - return nil - } -} - // NewDecFromString constructor -func NewDecFromString(s string, c ...SetupConstraint) (Dec, error) { +func NewDecFromString(s string) (Dec, error) { d, _, err := apd.NewFromString(s) if err != nil { return Dec{}, ErrInvalidDec.Wrap(err.Error()) @@ -89,16 +57,11 @@ func NewDecFromString(s string, c ...SetupConstraint) (Dec, error) { return Dec{}, ErrInvalidDec.Wrapf(s) default: result := Dec{*d} - for _, v := range c { - if err := v(result); err != nil { - return Dec{}, err - } - } return result, nil } } -func NewDecFromInt64(x int64, c ...SetupConstraint) Dec { +func NewDecFromInt64(x int64) Dec { var res Dec res.dec.SetInt64(x) return res diff --git a/math/dec_rapid_test.go b/math/dec_rapid_test.go index 27d6ba441f86..c4896159bfeb 100644 --- a/math/dec_rapid_test.go +++ b/math/dec_rapid_test.go @@ -115,7 +115,7 @@ func TestDecWithRapid(t *testing.T) { require.NoError(t, err) require.True(t, res.Equal(two)) - res, err = five.Rem(two) + res, err = five.Modulo(two) require.NoError(t, err) require.True(t, res.Equal(one)) diff --git a/math/dec_test.go b/math/dec_test.go index bc659383de15..d05afcf3e196 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -358,7 +358,7 @@ func TestSub(t *testing.T) { }, "1e100000 - 0 = 1e100000": { x: NewDecWithPrec(1, 100_000), - y: NewDecFromInt64(0111), + y: NewDecFromInt64(0), exp: must(NewDecFromString("1e100000")), }, "1e100001 - 0 -> err": { @@ -395,7 +395,7 @@ func TestSub(t *testing.T) { return } require.NoError(t, gotErr) - assert.True(t, spec.exp.Equal(got)) + assert.True(t, spec.exp.Equal(got), got.String()) }) } } @@ -810,7 +810,7 @@ func TestQuoInteger(t *testing.T) { "123 / 123 = 1": { x: NewDecFromInt64(123), y: NewDecFromInt64(123), - exp: NewDecFromInt64(5), + exp: NewDecFromInt64(1), }, "-123 / 123 = -1": { x: NewDecFromInt64(-123), @@ -1198,15 +1198,15 @@ func TestMulExact(t *testing.T) { y: NewDecWithPrec(1, 1), expErr: ErrInvalidDec, }, - "1e0000 * -1 = 1e0000": { + "1e0000 * -1 = -1e0000": { x: NewDecWithPrec(1, 100_000), y: NewDecWithPrec(-1, 0), - exp: NewDecWithPrec(1, 100_000), + exp: NewDecWithPrec(-1, 100_000), }, "1e100000 * -9 = 9e100000": { x: NewDecWithPrec(1, 100_000), y: NewDecFromInt64(-9), - exp: NewDecWithPrec(9, 100_000), + exp: NewDecWithPrec(-9, 100_000), }, "1e100000 * -10 = err": { x: NewDecWithPrec(1, 100_000), From 78937cc957fa808bd5fa05c684705e26fe15feba Mon Sep 17 00:00:00 2001 From: samricotta <37125168+samricotta@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:19:09 +0200 Subject: [PATCH 23/24] feat(math): Upstream GDA based decimal type (docs) (#20950) Co-authored-by: Alexander Peters --- math/dec.go | 145 ++++++++++++++++++++++++++++++++++++----- math/dec_rapid_test.go | 8 +-- math/dec_test.go | 28 ++++---- 3 files changed, 144 insertions(+), 37 deletions(-) diff --git a/math/dec.go b/math/dec.go index d355c6b1c4fa..2f858169c6fd 100644 --- a/math/dec.go +++ b/math/dec.go @@ -43,7 +43,22 @@ var dec128Context = apd.Context{ Traps: apd.DefaultTraps, } -// NewDecFromString constructor +// NewDecFromString converts a string to a Dec type, supporting standard, scientific, and negative notations. +// It handles non-numeric values and overflow conditions, returning errors for invalid inputs like "NaN" or "Infinity". +// +// Examples: +// - "123" -> Dec{123} +// - "-123.456" -> Dec{-123.456} +// - "1.23e4" -> Dec{12300} +// - "NaN" or "Infinity" -> ErrInvalidDec +// +// The internal representation is an arbitrary-precision decimal: Negative × Coeff × 10*Exponent +// The maximum exponent is 100_000 and must not be exceeded. Following values would be invalid: +// 1e100001 -> ErrInvalidDec +// -1e100001 -> ErrInvalidDec +// 1e-100001 -> ErrInvalidDec +// +// This function is essential for converting textual data into Dec types for numerical operations. func NewDecFromString(s string) (Dec, error) { d, _, err := apd.NewFromString(s) if err != nil { @@ -61,21 +76,41 @@ func NewDecFromString(s string) (Dec, error) { } } +// NewDecFromInt64 converts an int64 to a Dec type. +// This function is useful for creating Dec values from integer literals or variables, +// ensuring they can be used in high-precision arithmetic operations defined for Dec types. +// +// Example: +// - NewDecFromInt64(123) returns a Dec representing the value 123. func NewDecFromInt64(x int64) Dec { var res Dec res.dec.SetInt64(x) return res } -// NewDecWithPrec returns a decimal with a value of coeff * 10^exp precision. +// NewDecWithPrec creates a Dec from a coefficient and exponent, calculated as coeff * 10^exp. +// Useful for precise decimal representations. +// +// Example: +// - NewDecWithPrec(123, -2) -> Dec representing 1.23. func NewDecWithPrec(coeff int64, exp int32) Dec { var res Dec res.dec.SetFinite(coeff, exp) return res } -// Add returns a new Dec with value `x+y` without mutating any argument and error if -// there is an overflow. +// Add returns a new Dec representing the sum of `x` and `y` using returning a new Dec, we use apd.BaseContext. +// This function ensures that no arguments are mutated during the operation and checks for overflow conditions. +// If an overflow occurs, an error is returned. +// +// The precision is much higher as long as the max exponent is not exceeded. If the max exponent is exceeded, an error is returned. +// For example: +// - 1e100000 + -1e-1 +// - 1e100000 + 9e100000 +// - 1e100001 + 0 +// We can see that in apd.BaseContext the max exponent is defined hence we cannot exceed. +// +// This function wraps any internal errors with a context-specific error message for clarity. func (x Dec) Add(y Dec) (Dec, error) { var z Dec _, err := apd.BaseContext.Add(&z.dec, &x.dec, &y.dec) @@ -86,8 +121,20 @@ func (x Dec) Add(y Dec) (Dec, error) { return z, nil } -// Sub returns a new Dec with value `x-y` without mutating any argument and error if -// there is an overflow. +// Sub returns a new Dec representing the sum of `x` and `y` using returning a new Dec, we use apd.BaseContext. +// This function ensures that no arguments are mutated during the operation and checks for overflow conditions. +// If an overflow occurs, an error is returned. +// +// The precision is much higher as long as the max exponent is not exceeded. If the max exponent is exceeded, an error is returned. +// For example: +// - 1e-100001 - 0 +// - 1e100000 - 1e-1 +// - 1e100000 - -9e100000 +// - 1e100001 - 1e100001 (upper limit exceeded) +// - 1e-100001 - 1e-100001 (lower limit exceeded) +// We can see that in apd.BaseContext the max exponent is defined hence we cannot exceed. +// +// This function wraps any internal errors with a context-specific error message for clarity. func (x Dec) Sub(y Dec) (Dec, error) { var z Dec _, err := apd.BaseContext.Sub(&z.dec, &x.dec, &y.dec) @@ -99,8 +146,26 @@ func (x Dec) Sub(y Dec) (Dec, error) { return z, errors.Wrap(err, "decimal subtraction error") } -// 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. +// Quo performs division of x by y using the decimal128 context with 34 digits of precision. +// It returns a new Dec or an error if the division is not feasible due to constraints of decimal128. +// +// Within Quo half up rounding may be performed to match the defined precision. If this is unwanted, QuoExact +// should be used instead. +// +// Key error scenarios: +// - Division by zero (e.g., `123 / 0` or `0 / 0`) results in ErrInvalidDec. +// - Non-representable values due to extreme ratios or precision limits. +// +// Examples: +// - `0 / 123` yields `0`. +// - `123 / 123` yields `1.000000000000000000000000000000000`. +// - `-123 / 123` yields `-1.000000000000000000000000000000000`. +// - `4 / 9` yields `0.4444444444444444444444444444444444`. +// - `5 / 9` yields `0.5555555555555555555555555555555556`. +// - `6 / 9` yields `0.6666666666666666666666666666666667`. +// - `1e-100000 / 10` yields error. +// +// This function is non-mutative and enhances error clarity with specific messages. func (x Dec) Quo(y Dec) (Dec, error) { var z Dec _, err := dec128Context.Quo(&z.dec, &x.dec, &y.dec) @@ -111,8 +176,14 @@ func (x Dec) Quo(y Dec) (Dec, error) { return z, errors.Wrap(err, "decimal quotient error") } -// MulExact returns a new dec with value x * y. The product must not round or -// ErrUnexpectedRounding will be returned. +// MulExact multiplies two Dec values x and y without rounding, using decimal128 precision. +// It returns an error if rounding is necessary to fit the result within the 34-digit limit. +// +// Example: +// - MulExact(Dec{1.234}, Dec{2.345}) -> Dec{2.893}, or ErrUnexpectedRounding if precision exceeded. +// +// Note: +// - This function does not alter the original Dec values. func (x Dec) MulExact(y Dec) (Dec, error) { var z Dec condition, err := dec128Context.Mul(&z.dec, &x.dec, &y.dec) @@ -126,7 +197,25 @@ func (x Dec) MulExact(y Dec) (Dec, error) { return z, nil } -// QuoExact is a version of Quo that returns ErrUnexpectedRounding if any rounding occurred. +// QuoExact performs division like Quo and additionally checks for rounding. It returns ErrUnexpectedRounding if +// any rounding occurred during the division. If the division is exact, it returns the result without error. +// +// This function is particularly useful in financial calculations or other scenarios where precision is critical +// and rounding could lead to significant errors. +// +// Key error scenarios: +// - Division by zero (e.g., `123 / 0` or `0 / 0`) results in ErrInvalidDec. +// - Rounding would have occurred, which is not permissible in this context, resulting in ErrUnexpectedRounding. +// +// Examples: +// - `0 / 123` yields `0` without rounding. +// - `123 / 123` yields `1.000000000000000000000000000000000` exactly. +// - `-123 / 123` yields `-1.000000000000000000000000000000000` exactly. +// - `1 / 9` yields error for the precision limit +// - `1e-100000 / 10` yields error for crossing the lower exponent limit. +// - Any division resulting in a non-terminating decimal under decimal128 precision constraints triggers ErrUnexpectedRounding. +// +// This function does not mutate any arguments and wraps any internal errors with a context-specific error message for clarity. func (x Dec) QuoExact(y Dec) (Dec, error) { var z Dec condition, err := dec128Context.Quo(&z.dec, &x.dec, &y.dec) @@ -139,8 +228,20 @@ func (x Dec) QuoExact(y Dec) (Dec, error) { return z, errors.Wrap(err, "decimal quotient error") } -// 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. +// QuoInteger performs integer division of x by y, returning a new Dec formatted as decimal128 with 34 digit precision. +// This function returns the integer part of the quotient, discarding any fractional part, and is useful in scenarios +// where only the whole number part of the division result is needed without rounding. +// +// Key error scenarios: +// - Division by zero (e.g., `123 / 0`) results in ErrInvalidDec. +// - Overflow conditions if the result exceeds the storage capacity of a decimal128 formatted number. +// +// Examples: +// - `123 / 50` yields `2` (since the fractional part .46 is discarded). +// - `100 / 3` yields `33` (since the fractional part .3333... is discarded). +// - `50 / 100` yields `0` (since 0.5 is less than 1 and thus discarded). +// +// The function does not mutate any arguments and ensures that errors are wrapped with specific messages for clarity. func (x Dec) QuoInteger(y Dec) (Dec, error) { var z Dec _, err := dec128Context.QuoInteger(&z.dec, &x.dec, &y.dec) @@ -150,8 +251,8 @@ func (x Dec) QuoInteger(y Dec) (Dec, error) { return z, nil } -// Modulo 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 +// Modulo computes the remainder of division of x by y using decimal128 precision. +// It returns an error if y is zero or if any other error occurs during the computation. func (x Dec) Modulo(y Dec) (Dec, error) { var z Dec _, err := dec128Context.Rem(&z.dec, &x.dec, &y.dec) @@ -187,8 +288,9 @@ func (x Dec) BigInt() (*big.Int, error) { 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 +// SdkIntTrim rounds the decimal number towards zero to the nearest integer, then converts and returns it as `sdkmath.Int`. +// It handles both positive and negative values correctly by truncating towards zero. +// This function panics if the resulting integer is larger than the maximum value that `sdkmath.Int` can represent. func (x Dec) SdkIntTrim() Int { y, _ := x.Reduce() r := y.dec.Coeff @@ -221,6 +323,8 @@ func (x Dec) Cmp(y Dec) int { return x.dec.Cmp(&y.dec) } +// Equal checks if the decimal values of x and y are exactly equal. +// It returns true if both decimals represent the same value, otherwise false. func (x Dec) Equal(y Dec) bool { return x.dec.Cmp(&y.dec) == 0 } @@ -254,17 +358,22 @@ func (x Dec) NumDecimalPlaces() uint32 { return uint32(-exp) } -// Reduce returns a copy of x with all trailing zeros removed +// Reduce returns a copy of x with all trailing zeros removed and the number of zeros that were removed. +// It does not modify the original decimal. func (x Dec) Reduce() (Dec, int) { y := Dec{} _, n := y.dec.Reduce(&x.dec) return y, n } +// Marshal serializes the decimal value into a byte slice in a text format. +// This method ensures the decimal is represented in a portable and human-readable form. func (d Dec) Marshal() ([]byte, error) { return d.dec.MarshalText() } +// Unmarshal parses a byte slice containing a text-formatted decimal and stores the result in the receiver. +// It returns an error if the byte slice does not represent a valid decimal. func (d *Dec) Unmarshal(data []byte) error { return d.dec.UnmarshalText(data) } diff --git a/math/dec_rapid_test.go b/math/dec_rapid_test.go index c4896159bfeb..a33f82a4fdde 100644 --- a/math/dec_rapid_test.go +++ b/math/dec_rapid_test.go @@ -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 := NewDecFromString(s, AssertNotNegative()) + _, err := NewDecFromString(s) require.Error(t, err) } @@ -215,7 +215,7 @@ func testInvalidNewNonNegativeFixedDecFromString(t *rapid.T) { ), rapid.StringMatching(fmt.Sprintf(`\d*\.\d{%d,}`, n+1)), ).Draw(t, "s") - _, err := NewDecFromString(s, AssertNotNegative(), AssertMaxDecimals(n)) + _, err := NewDecFromString(s) require.Error(t, err) } @@ -226,7 +226,7 @@ func testInvalidNewPositiveDecFromString(t *rapid.T) { rapid.StringMatching("[[:alpha:]]+"), rapid.StringMatching(`^-\d*\.?\d+|0$`), ).Draw(t, "s") - _, err := NewDecFromString(s, AssertGreaterThanZero()) + _, err := NewDecFromString(s) require.Error(t, err) } @@ -239,7 +239,7 @@ func testInvalidNewPositiveFixedDecFromString(t *rapid.T) { rapid.StringMatching(`^-\d*\.?\d+|0$`), rapid.StringMatching(fmt.Sprintf(`\d*\.\d{%d,}`, n+1)), ).Draw(t, "s") - _, err := NewDecFromString(s, AssertGreaterThanZero(), AssertMaxDecimals(n)) + _, err := NewDecFromString(s) require.Error(t, err) } diff --git a/math/dec_test.go b/math/dec_test.go index d05afcf3e196..fb7134b06cad 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -13,10 +13,9 @@ import ( func TestNewDecFromString(t *testing.T) { specs := map[string]struct { - src string - constraints []SetupConstraint - exp Dec - expErr error + src string + exp Dec + expErr error }{ "simple decimal": { src: "1", @@ -239,13 +238,13 @@ func TestAdd(t *testing.T) { y: NewDecFromInt64(1), exp: must(NewDecWithPrec(1, 100_000).Add(NewDecFromInt64(1))), }, - "1e100000 + 0 -> err": { + "1e100001 + 0 -> err": { x: NewDecWithPrec(1, 100_001), y: NewDecFromInt64(0), expErr: ErrInvalidDec, }, - "-1e100000 + 0 -> err": { - x: NewDecWithPrec(-1, 100_001), + "-1e100001 + 0 -> err": { + x: NewDecWithPrec(1, 100_001), y: NewDecFromInt64(0), expErr: ErrInvalidDec, }, @@ -265,11 +264,10 @@ func TestAdd(t *testing.T) { func TestSub(t *testing.T) { specs := map[string]struct { - x Dec - y Dec - exp Dec - expErr error - constraints []SetupConstraint + x Dec + y Dec + exp Dec + expErr error }{ "0 - 0 = 0": { x: NewDecFromInt64(0), @@ -367,9 +365,9 @@ func TestSub(t *testing.T) { expErr: ErrInvalidDec, }, "1e100000 - -1 -> 100..1": { - x: NewDecWithPrec(1, 100_000), - y: NewDecFromInt64(-1), - exp: must(NewDecFromString("1" + strings.Repeat("0", 99_999) + "1")), + x: NewDecWithPrec(1, 100_000), + y: must(NewDecFromString("-9e100000")), + expErr: ErrInvalidDec, }, "1e-100000 - 0 = 1e-100000": { x: NewDecWithPrec(1, -100_000), From e96b382d351aca627921474009ed2035d3983d71 Mon Sep 17 00:00:00 2001 From: samricotta Date: Wed, 24 Jul 2024 12:08:18 +0200 Subject: [PATCH 24/24] Update 18-decimal-handling.md --- .../building-modules/18-decimal-handling.md | 86 ++++--------------- 1 file changed, 16 insertions(+), 70 deletions(-) diff --git a/docs/build/building-modules/18-decimal-handling.md b/docs/build/building-modules/18-decimal-handling.md index c3b6a2eb3e86..ac759127c17d 100644 --- a/docs/build/building-modules/18-decimal-handling.md +++ b/docs/build/building-modules/18-decimal-handling.md @@ -14,39 +14,33 @@ 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. -* **Better Performance**: `Dec` operations are faster and more efficient than `LegacyDec`.` +* Historically we have wrapped a `big.Int` to represent decimals in the Cosmos SDK and never had a decimal type. Finally, we have a decimal type that is more efficient and accurate. +* `Dec` uses the [apd](https://github.com/cockroachdb/apd) library for arbitrary precision decimals, suitable for accurate financial calculations. +* `Dec` operations are safer for concurrent use as they do not mutate the original values. +* `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. +-- math.NewLegacyDecFromInt64(100) +++ math.NewDecFromInt64(100) +-- math.LegacyNewDecWithPrec(100, 18) +++ math.NewDecWithPrec(100, 18) -```protobuf -message DecCoin { - option (gogoproto.equal) = true; +-- math.LegacyNewDecFromStr("100") +++ math.NewDecFromStr("100") - string denom = 1; - string amount = 2 [ - (cosmos_proto.scalar) = "cosmos.Dec", - (gogoproto.customtype) = "cosmossdk.io/math.Dec", - (gogoproto.nullable) = false - ]; -} -``` +-- math.LegacyNewDecFromStr("100.000000000000000000").Quo(math.LegacyNewDecFromInt(2)) +++ math.NewDecFromStr("100.000000000000000000").Quo(math.NewDecFromInt(2)) -How you can implement `Dec` in your module: +-- math.LegacyNewDecFromStr("100.000000000000000000").Add(math.LegacyNewDecFromInt(2)) +++ math.NewDecFromStr("100.000000000000000000").Add(math.NewDecFromInt(2)) -```go -import ( - "cosmossdk.io/math" -) +-- math.LegacyNewDecFromStr("100.000000000000000000").Sub(math.LegacyNewDecFromInt(2)) +++ math.NewDecFromStr("100.000000000000000000").Sub(math.NewDecFromInt(2)) -example := math.NewDecFromInt64(100) -``` ## Modules migrating from `LegacyDec` to `Dec` @@ -59,58 +53,10 @@ The reason for the state breaking change is the difference in precision handling * **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` - -This example includes the value 123456789 followed by 18 zeros to maintain the fixed precision. - -**New Dec Byte Representation:** -`0a03617364121031323334353637383900000000000000` - -This example shows the value 123456789 without additional padding, reflecting the flexible precision handling of the new Dec type. - ## 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:** - -```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.