diff --git a/CHANGELOG.md b/CHANGELOG.md index b749d122f7a9..655f3a27cb79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,11 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [v0.37.5] - TBD +### Features + +* (types) [\#5360](https://github.com/cosmos/cosmos-sdk/pull/5360) Implement `SortableDecBytes` which + allows the `Dec` type be sortable. + ### Bug Fixes * (types) [\#5395](https://github.com/cosmos/cosmos-sdk/issues/5395) Fix `Uint#LTE`. diff --git a/types/decimal.go b/types/decimal.go index 1a16cebc83fa..c7c5f197d83e 100644 --- a/types/decimal.go +++ b/types/decimal.go @@ -527,6 +527,42 @@ func (d Dec) Ceil() Dec { //___________________________________________________________________________________ +// MaxSortableDec is the largest Dec that can be passed into SortableDecBytes() +// Its negative form is the least Dec that can be passed in. +var MaxSortableDec = OneDec().Quo(SmallestDec()) + +// ValidSortableDec 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 ValidSortableDec(dec Dec) bool { + return dec.Abs().LTE(MaxSortableDec) +} + +// SortableDecBytes 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 SortableDecBytes(dec Dec) []byte { + if !ValidSortableDec(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(MaxSortableDec) { + return []byte("max") + } + // For the same reason, we make the bytes of minimum sortable dec be --, which comes before all numbers. + if dec.Equal(MaxSortableDec.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", Precision*2+1), dec.Abs().String()))...) + } + return []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", Precision*2+1), dec.String())) +} + +//___________________________________________________________________________________ + // reuse nil values var ( nilAmino string diff --git a/types/decimal_test.go b/types/decimal_test.go index 55aa31db9b58..301ddd0c4293 100644 --- a/types/decimal_test.go +++ b/types/decimal_test.go @@ -422,3 +422,28 @@ func TestDecCeil(t *testing.T) { require.Equal(t, tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input) } } + +func TestDecSortableBytes(t *testing.T) { + tests := []struct { + d Dec + want []byte + }{ + {NewDec(0), []byte("000000000000000000.000000000000000000")}, + {NewDec(1), []byte("000000000000000001.000000000000000000")}, + {NewDec(10), []byte("000000000000000010.000000000000000000")}, + {NewDec(12340), []byte("000000000000012340.000000000000000000")}, + {NewDecWithPrec(12340, 4), []byte("000000000000000001.234000000000000000")}, + {NewDecWithPrec(12340, 5), []byte("000000000000000000.123400000000000000")}, + {NewDecWithPrec(12340, 8), []byte("000000000000000000.000123400000000000")}, + {NewDecWithPrec(1009009009009009009, 17), []byte("000000000000000010.090090090090090090")}, + {NewDecWithPrec(-1009009009009009009, 17), []byte("-000000000000000010.090090090090090090")}, + {NewDec(1000000000000000000), []byte("max")}, + {NewDec(-1000000000000000000), []byte("--")}, + } + for tcIndex, tc := range tests { + assert.Equal(t, tc.want, SortableDecBytes(tc.d), "bad String(), index: %v", tcIndex) + } + + assert.Panics(t, func() { SortableDecBytes(NewDec(1000000000000000001)) }) + assert.Panics(t, func() { SortableDecBytes(NewDec(-1000000000000000001)) }) +}