Skip to content

Commit

Permalink
feat(math): Upstream GDA based decimal type (docs) (#20950)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Peters <[email protected]>
  • Loading branch information
samricotta and alpe committed Jul 19, 2024
1 parent 777b298 commit 78937cc
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 37 deletions.
145 changes: 127 additions & 18 deletions math/dec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
8 changes: 4 additions & 4 deletions math/dec_rapid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
}

Expand All @@ -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)
}

Expand All @@ -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)
}

Expand Down
28 changes: 13 additions & 15 deletions math/dec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
},
Expand All @@ -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),
Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit 78937cc

Please sign in to comment.