diff --git a/math/CHANGELOG.md b/math/CHANGELOG.md index b7de93d928..23c86a8074 100644 --- a/math/CHANGELOG.md +++ b/math/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.j ### Features +* [#24343](https://github.com/cosmos/cosmos-sdk/pull/24343) feat(math/Dec): Add comparison and utility APIs MinDec, MaxDec, Neg(), Abs(), GT(), GTE(), LT(), LTE(). * [#24229](https://github.com/cosmos/cosmos-sdk/pull/24229) Add `DecFromLegacyDec` migration function. ## [math/v1.5.1](https://github.com/cosmos/cosmos-sdk/releases/tag/math/v1.5.1) - 2025-03-28 diff --git a/math/dec.go b/math/dec.go index 4935fb2eb9..32a49c1af7 100644 --- a/math/dec.go +++ b/math/dec.go @@ -254,6 +254,26 @@ func (x Dec) Mul(y Dec) (Dec, error) { return z, nil } +// Neg returns a new Dec with value `-x` (negation of x) without mutating the argument x. +// It returns an error if the negation operation fails. +func (x Dec) Neg() (Dec, error) { + var z Dec + if _, err := dec128Context.Neg(&z.dec, &x.dec); err != nil { + return z, ErrInvalidDec.Wrap(err.Error()) + } + return z, nil +} + +// Abs returns a new Dec with the absolute value of x without mutating the argument x. +// It returns an error if the absolute value operation fails. +func (x Dec) Abs() (Dec, error) { + var z Dec + if _, err := dec128Context.Abs(&z.dec, &x.dec); err != nil { + return z, ErrInvalidDec.Wrap(err.Error()) + } + return z, nil +} + // 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. // @@ -365,6 +385,26 @@ func (x Dec) Cmp(y Dec) int { return x.dec.Cmp(&y.dec) } +// LT (less than) returns true if x is less than y, false otherwise +func (x Dec) LT(y Dec) bool { + return x.Cmp(y) == -1 +} + +// LTE (less than or equal) returns true if x is less than or equal to y, false otherwise +func (x Dec) LTE(y Dec) bool { + return x.Cmp(y) != 1 +} + +// GT (greater than) returns true if x is greater than y, false otherwise +func (x Dec) GT(y Dec) bool { + return x.Cmp(y) == 1 +} + +// GTE (greater than or equal) returns true if x is greater than or equal to y, false otherwise +func (x Dec) GTE(y Dec) bool { + return x.Cmp(y) != -1 +} + // 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 { @@ -520,3 +560,19 @@ func (x *Dec) UnmarshalJSON(data []byte) error { *x = val return nil } + +// MinDec returns the smaller of x and y +func MinDec(x, y Dec) Dec { + if x.LT(y) { + return x + } + return y +} + +// MaxDec returns the larger of x and y +func MaxDec(x, y Dec) Dec { + if x.GT(y) { + return x + } + return y +} diff --git a/math/dec_test.go b/math/dec_test.go index 2aa1a84dc4..a4c84cef52 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -1453,3 +1453,485 @@ func TestMarshalUnmarshal(t *testing.T) { }) } } + +func TestLT(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + exp bool + }{ + "0 == 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: false, + }, + "0 < 123": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: true, + }, + "123 > 0": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + exp: false, + }, + "-123 < 0": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + exp: true, + }, + "123 == 123": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: false, + }, + "-123 == -123": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: false, + }, + "1.234 == 1.234": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1234, -3), + exp: false, + }, + "1.234 > 1.233": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1233, -3), + exp: false, + }, + "1.233 < 1.234": { + x: NewDecWithExp(1233, -3), + y: NewDecWithExp(1234, -3), + exp: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := spec.x.LT(spec.y) + assert.Equal(t, spec.exp, got, "x: %s, y: %s", spec.x.String(), spec.y.String()) + }) + } +} + +func TestLTE(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + exp bool + }{ + "0 == 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: true, + }, + "0 < 123": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: true, + }, + "123 > 0": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + exp: false, + }, + "-123 < 0": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + exp: true, + }, + "123 == 123": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: true, + }, + "-123 == -123": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: true, + }, + "1.234 == 1.234": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1234, -3), + exp: true, + }, + "1.234 > 1.233": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1233, -3), + exp: false, + }, + "1.233 < 1.234": { + x: NewDecWithExp(1233, -3), + y: NewDecWithExp(1234, -3), + exp: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := spec.x.LTE(spec.y) + assert.Equal(t, spec.exp, got, "x: %s, y: %s", spec.x.String(), spec.y.String()) + }) + } +} + +func TestGT(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + exp bool + }{ + "0 == 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: false, + }, + "0 < 123": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: false, + }, + "123 > 0": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + exp: true, + }, + "-123 < 0": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + exp: false, + }, + "123 == 123": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: false, + }, + "-123 == -123": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: false, + }, + "1.234 == 1.234": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1234, -3), + exp: false, + }, + "1.234 > 1.233": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1233, -3), + exp: true, + }, + "1.233 < 1.234": { + x: NewDecWithExp(1233, -3), + y: NewDecWithExp(1234, -3), + exp: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := spec.x.GT(spec.y) + assert.Equal(t, spec.exp, got, "x: %s, y: %s", spec.x.String(), spec.y.String()) + }) + } +} + +func TestGTE(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + exp bool + }{ + "0 == 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: true, + }, + "0 < 123": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: false, + }, + "123 > 0": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + exp: true, + }, + "-123 < 0": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + exp: false, + }, + "123 == 123": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: true, + }, + "-123 == -123": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: true, + }, + "1.234 == 1.234": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1234, -3), + exp: true, + }, + "1.234 > 1.233": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1233, -3), + exp: true, + }, + "1.233 < 1.234": { + x: NewDecWithExp(1233, -3), + y: NewDecWithExp(1234, -3), + exp: false, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := spec.x.GTE(spec.y) + assert.Equal(t, spec.exp, got, "x: %s, y: %s", spec.x.String(), spec.y.String()) + }) + } +} + +func TestMinDec(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + exp Dec + }{ + "0 == 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: NewDecFromInt64(0), + }, + "0 < 123": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(0), + }, + "123 > 0": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + exp: NewDecFromInt64(0), + }, + "-123 < 0": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + exp: NewDecFromInt64(-123), + }, + "123 == 123": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(123), + }, + "-123 == -123": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: NewDecFromInt64(-123), + }, + "1.234 == 1.234": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1234, -3), + exp: NewDecWithExp(1234, -3), + }, + "1.234 > 1.233": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1233, -3), + exp: NewDecWithExp(1233, -3), + }, + "1.233 < 1.234": { + x: NewDecWithExp(1233, -3), + y: NewDecWithExp(1234, -3), + exp: NewDecWithExp(1233, -3), + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := MinDec(spec.x, spec.y) + assert.Equal(t, spec.exp, got, "x: %s, y: %s", spec.x.String(), spec.y.String()) + }) + } +} + +func TestMaxDec(t *testing.T) { + specs := map[string]struct { + x Dec + y Dec + exp Dec + }{ + "0 == 0": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(0), + exp: NewDecFromInt64(0), + }, + "0 < 123 ": { + x: NewDecFromInt64(0), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(123), + }, + "123 > 0 ": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(0), + exp: NewDecFromInt64(123), + }, + "-123 < 0": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(0), + exp: NewDecFromInt64(0), + }, + "123 == 123": { + x: NewDecFromInt64(123), + y: NewDecFromInt64(123), + exp: NewDecFromInt64(123), + }, + "-123 == -123": { + x: NewDecFromInt64(-123), + y: NewDecFromInt64(-123), + exp: NewDecFromInt64(-123), + }, + "1.234 == 1.234": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1234, -3), + exp: NewDecWithExp(1234, -3), + }, + "1.234 > 1.233": { + x: NewDecWithExp(1234, -3), + y: NewDecWithExp(1233, -3), + exp: NewDecWithExp(1234, -3), + }, + "1.233 < 1.234": { + x: NewDecWithExp(1233, -3), + y: NewDecWithExp(1234, -3), + exp: NewDecWithExp(1234, -3), + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got := MaxDec(spec.x, spec.y) + assert.Equal(t, spec.exp, got, "x: %s, y: %s", spec.x.String(), spec.y.String()) + }) + } +} + +func TestNeg(t *testing.T) { + specs := map[string]struct { + x Dec + exp Dec + expErr error + }{ + "0": { + x: NewDecFromInt64(0), + exp: NewDecFromInt64(0), + }, + "123": { + x: NewDecFromInt64(123), + exp: NewDecFromInt64(-123), + }, + "-123": { + x: NewDecFromInt64(-123), + exp: NewDecFromInt64(123), + }, + "1.234": { + x: NewDecWithExp(1234, -3), + exp: NewDecWithExp(-1234, -3), + }, + "-1.234 ": { + x: NewDecWithExp(-1234, -3), + exp: NewDecWithExp(1234, -3), + }, + + "1e100000": { + x: NewDecWithExp(1, 100_000), + exp: NewDecWithExp(-1, 100_000), + }, + "-9e900000 -> Err": { + x: NewDecWithExp(-9, 900_000), + expErr: ErrInvalidDec, + }, + "-1e^-1": { + x: NewDecWithExp(-1, -1), + exp: NewDecWithExp(1, -1), + }, + "-1e100001": { + x: NewDecWithExp(-1, 100_001), + expErr: ErrInvalidDec, + }, + "1e100001": { + x: NewDecWithExp(1, 100_001), + expErr: ErrInvalidDec, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := spec.x.Neg() + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr, got) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestAbs(t *testing.T) { + specs := map[string]struct { + x Dec + exp Dec + expErr error + }{ + "0": { + x: NewDecFromInt64(0), + exp: NewDecFromInt64(0), + }, + "123": { + x: NewDecFromInt64(123), + exp: NewDecFromInt64(123), + }, + "-123": { + x: NewDecFromInt64(-123), + exp: NewDecFromInt64(123), + }, + "1.234": { + x: NewDecWithExp(1234, -3), + exp: NewDecWithExp(1234, -3), + }, + "-1.234 ": { + x: NewDecWithExp(-1234, -3), + exp: NewDecWithExp(1234, -3), + }, + + "1e100000": { + x: NewDecWithExp(1, 100_000), + exp: NewDecWithExp(1, 100_000), + }, + "-9e900000 -> Err": { + x: NewDecWithExp(-9, 900_000), + expErr: ErrInvalidDec, + }, + "-1e^-1": { + x: NewDecWithExp(-1, -1), + exp: NewDecWithExp(1, -1), + }, + "-1e100001": { + x: NewDecWithExp(-1, 100_001), + expErr: ErrInvalidDec, + }, + "1e100001": { + x: NewDecWithExp(1, 100_001), + expErr: ErrInvalidDec, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + got, gotErr := spec.x.Abs() + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr, got) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.exp, got) + }) + } +}