Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(math): Upstream GDA based decimal type (docs) #20950

Merged
merged 3 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
samricotta marked this conversation as resolved.
Show resolved Hide resolved
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.
samricotta marked this conversation as resolved.
Show resolved Hide resolved
// 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.
samricotta marked this conversation as resolved.
Show resolved Hide resolved
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
Loading