From 3587c376157d6c6343e75d6d52a0d4cd538f9e83 Mon Sep 17 00:00:00 2001 From: samricotta Date: Mon, 15 Jul 2024 10:53:58 +0200 Subject: [PATCH 1/3] Update dec.go --- math/dec.go | 128 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 18 deletions(-) diff --git a/math/dec.go b/math/dec.go index d355c6b1c4fa..fceb7e962e87 100644 --- a/math/dec.go +++ b/math/dec.go @@ -43,7 +43,16 @@ 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 +// +// 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 +70,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 decimal128 precision. +// This function ensures that no arguments are mutated during the operation and checks for overflow conditions. +// If an overflow occurs, an error is returned. +// +// Key error scenarios: +// - Overflow if the sum exceeds the maximum representable value within decimal128 precision. +// +// Examples: +// - `MaxDec + 1` results in an overflow error. +// - `123.456 + 654.321` yields `777.777`. +// +// 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 +115,18 @@ 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 difference `x-y` using decimal128 precision. +// This function ensures that no arguments are mutated and checks for overflow conditions. +// If an overflow occurs, or if subtraction results in a non-representable value, an error is returned. +// +// Key error scenarios: +// - Overflow if the difference is less than the minimum representable value within decimal128 precision. +// +// Examples: +// - `MinDec - 1` results in an overflow error. +// - `1000.000 - 500.000` yields `500.000`. +// +// Errors are wrapped to enhance error clarity, providing specific messages related to subtraction operations. func (x Dec) Sub(y Dec) (Dec, error) { var z Dec _, err := apd.BaseContext.Sub(&z.dec, &x.dec, &y.dec) @@ -99,8 +138,19 @@ 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. +// +// 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`. +// +// 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 +161,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 +182,23 @@ 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 but 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. +// - 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 +211,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 +234,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 +271,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 +306,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 +341,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) } From 1ad2e809a26e846ecbfe03953c6f8fc01c28d173 Mon Sep 17 00:00:00 2001 From: samricotta Date: Wed, 17 Jul 2024 16:26:02 +0200 Subject: [PATCH 2/3] Update dec.go --- math/dec.go | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/math/dec.go b/math/dec.go index fceb7e962e87..6681c294c5ac 100644 --- a/math/dec.go +++ b/math/dec.go @@ -52,6 +52,12 @@ var dec128Context = apd.Context{ // - "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 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) @@ -93,16 +99,16 @@ func NewDecWithPrec(coeff int64, exp int32) Dec { return res } -// Add returns a new Dec representing the sum of `x` and `y` using decimal128 precision. +// 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. // -// Key error scenarios: -// - Overflow if the sum exceeds the maximum representable value within decimal128 precision. -// -// Examples: -// - `MaxDec + 1` results in an overflow error. -// - `123.456 + 654.321` yields `777.777`. +// 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 + -9e900000 +// - 1e100000 + 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) { @@ -115,18 +121,19 @@ func (x Dec) Add(y Dec) (Dec, error) { return z, nil } -// Sub returns a new Dec representing the difference `x-y` using decimal128 precision. -// This function ensures that no arguments are mutated and checks for overflow conditions. -// If an overflow occurs, or if subtraction results in a non-representable value, an error is returned. -// -// Key error scenarios: -// - Overflow if the difference is less than the minimum representable value within decimal128 precision. +// 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. // -// Examples: -// - `MinDec - 1` results in an overflow error. -// - `1000.000 - 500.000` yields `500.000`. +// 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 - 1^1 +// - 1e100001 - 1e100001 (upper limit exceeded) +// - -100_001 - -100_001 (lower liit exceeded) +// We can see that in apd.BaseContext the max exponent is defined hence we cannot exceed. // -// Errors are wrapped to enhance error clarity, providing specific messages related to subtraction operations. +// 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) @@ -141,6 +148,9 @@ func (x Dec) Sub(y Dec) (Dec, error) { // 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 rounding is performed and specifies the Rounder in the context to use during the rounding function. +// RoundHalfUp is used if empty or not present in Roundings. +// // 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. @@ -182,7 +192,7 @@ func (x Dec) MulExact(y Dec) (Dec, error) { return z, nil } -// QuoExact performs division like Quo but additionally checks for rounding. It returns ErrUnexpectedRounding if +// QuoExact performs division like Quo andadditionally 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 From f8ac2f7536125fcdf6d5a268506277449fbdcc9a Mon Sep 17 00:00:00 2001 From: Alexander Peters Date: Thu, 18 Jul 2024 16:06:20 +0200 Subject: [PATCH 3/3] Review comments (#20987) --- math/dec.go | 25 ++++++++++++++++--------- math/dec_rapid_test.go | 8 ++++---- math/dec_test.go | 28 +++++++++++++--------------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/math/dec.go b/math/dec.go index 6681c294c5ac..2f858169c6fd 100644 --- a/math/dec.go +++ b/math/dec.go @@ -53,7 +53,7 @@ var dec128Context = apd.Context{ // - "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 exceeded. Following values would be invalid: +// The maximum exponent is 100_000 and must not be exceeded. Following values would be invalid: // 1e100001 -> ErrInvalidDec // -1e100001 -> ErrInvalidDec // 1e-100001 -> ErrInvalidDec @@ -105,9 +105,9 @@ func NewDecWithPrec(coeff int64, exp int32) Dec { // // 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 + -9e900000 -// - 1e100000 + 0 +// - 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. @@ -128,9 +128,10 @@ func (x Dec) Add(y Dec) (Dec, error) { // 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 - 1^1 +// - 1e100000 - 1e-1 +// - 1e100000 - -9e100000 // - 1e100001 - 1e100001 (upper limit exceeded) -// - -100_001 - -100_001 (lower liit 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. @@ -148,8 +149,8 @@ func (x Dec) Sub(y Dec) (Dec, error) { // 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 rounding is performed and specifies the Rounder in the context to use during the rounding function. -// RoundHalfUp is used if empty or not present in Roundings. +// 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. @@ -159,6 +160,10 @@ func (x Dec) Sub(y Dec) (Dec, error) { // - `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) { @@ -192,7 +197,7 @@ func (x Dec) MulExact(y Dec) (Dec, error) { return z, nil } -// QuoExact performs division like Quo andadditionally checks for rounding. It returns ErrUnexpectedRounding if +// 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 @@ -206,6 +211,8 @@ func (x Dec) MulExact(y Dec) (Dec, error) { // - `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. 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),