From 0c74aee0c1c5884862636d065f00a202914b6eaf Mon Sep 17 00:00:00 2001 From: Vadim Graboys Date: Sun, 14 Jun 2015 11:53:23 -0400 Subject: [PATCH] add Round, Floor, Ceil, and StringFixed. Deprecate StringScaled --- decimal.go | 87 +++++++++++++++++++++++++++---- decimal_test.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 9 deletions(-) diff --git a/decimal.go b/decimal.go index a6e7d56..93df2f0 100644 --- a/decimal.go +++ b/decimal.go @@ -46,8 +46,10 @@ var DivisionPrecision = 16 // Zero constant, to make computations faster. var Zero = New(0, 1) -var tenInt = big.NewInt(10) +var zeroInt = big.NewInt(0) var oneInt = big.NewInt(1) +var fiveInt = big.NewInt(5) +var tenInt = big.NewInt(10) // Decimal represents a fixed-point decimal. It is immutable. // number = value * 10 ^ exp @@ -332,6 +334,10 @@ func (d Decimal) Float64() (f float64, exact bool) { // -12.345 // func (d Decimal) String() string { + return d.string(true) +} + +func (d Decimal) string(trimTrailingZeros bool) string { if d.exp >= 0 { return d.rescale(0).value.String() } @@ -351,13 +357,15 @@ func (d Decimal) String() string { fractionalPart = strings.Repeat("0", num0s) + str } - i := len(fractionalPart) - 1 - for ; i >= 0; i-- { - if fractionalPart[i] != '0' { - break + if trimTrailingZeros { + i := len(fractionalPart) - 1 + for ; i >= 0; i-- { + if fractionalPart[i] != '0' { + break + } } + fractionalPart = fractionalPart[:i+1] } - fractionalPart = fractionalPart[:i+1] number := intPart if len(fractionalPart) > 0 { @@ -371,9 +379,64 @@ func (d Decimal) String() string { return number } -// StringScaled first scales the decimal then calls .String() on it. -func (d Decimal) StringScaled(exp int32) string { - return d.rescale(exp).String() +// StringFixed returns a rounded fixed-point string with places digits after +// the decimal point. +// +// Example: +// +// NewFromFloat(0).StringFixed(2) // output: "0.00" +// NewFromFloat(0).StringFixed(0) // output: "0" +// NewFromFloat(5.45).StringFixed(0) // output: "5" +// NewFromFloat(5.45).StringFixed(1) // output: "5.5" +// NewFromFloat(5.45).StringFixed(2) // output: "5.45" +// NewFromFloat(5.45).StringFixed(3) // output: "5.450" +// NewFromFloat(545).StringFixed(-1) // output: "550" +// +func (d Decimal) StringFixed(places int32) string { + rounded := d.Round(places) + return rounded.string(false) +} + +// Round rounds the decimal to places decimal places. +// If places < 0, it will round the integer part to the nearest 10^(-places). +// +// Example: +// +// NewFromFloat(5.45).Round(1).String() // output: "5.5" +// NewFromFloat(545).Round(-1).String() // output: "550" +// +func (d Decimal) Round(places int32) Decimal { + almost := d.rescale(-(places + 1)) + if almost.value.Sign() < 0 { + almost.value.Sub(almost.value, fiveInt) + } else { + almost.value.Add(almost.value, fiveInt) + } + + _, m := almost.value.DivMod(almost.value, tenInt, new(big.Int)) + almost.exp += 1 + if almost.value.Sign() < 0 && m.Cmp(zeroInt) != 0 { + almost.value.Add(almost.value, oneInt) + } + + return almost +} + +func (d Decimal) Floor() Decimal { + exp := big.NewInt(10) + exp.Exp(exp, big.NewInt(int64(-d.exp)), nil) + z := new(big.Int).Div(d.value, exp) + return Decimal{value: z, exp: 0} +} + +func (d Decimal) Ceil() Decimal { + exp := big.NewInt(10) + exp.Exp(exp, big.NewInt(int64(-d.exp)), nil) + z, m := new(big.Int).DivMod(d.value, exp, new(big.Int)) + if m.Cmp(zeroInt) != 0 { + z.Add(z, oneInt) + } + return Decimal{value: z, exp: 0} } // UnmarshalJSON implements the json.Unmarshaler interface. @@ -447,6 +510,12 @@ func (d Decimal) MarshalText() (text []byte, err error) { return []byte(d.String()), nil } +// NOTE: buggy, unintuitive, and DEPRECATED! Use StringFixed instead. +// StringScaled first scales the decimal then calls .String() on it. +func (d Decimal) StringScaled(exp int32) string { + return d.rescale(exp).String() +} + func (d *Decimal) ensureInitialized() { if d.value == nil { d.value = new(big.Int) diff --git a/decimal_test.go b/decimal_test.go index e4f82fb..a9d14d1 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -2,6 +2,7 @@ package decimal import ( "math" + "strings" "testing" ) @@ -189,6 +190,141 @@ func TestDecimal_rescale(t *testing.T) { } } +func TestDecimal_Floor(t *testing.T) { + type testData struct { + input string + expected string + } + tests := []testData{ + {"1.999", "1"}, + {"1", "1"}, + {"1.01", "1"}, + {"0", "0"}, + {"0.9", "0"}, + {"0.1", "0"}, + {"-0.9", "-1"}, + {"-0.1", "-1"}, + {"-1.00", "-1"}, + {"-1.01", "-2"}, + {"-1.999", "-2"}, + } + for _, test := range tests { + d, _ := NewFromString(test.input) + expected, _ := NewFromString(test.expected) + got := d.Floor() + if !got.Equals(expected) { + t.Errorf("Floor(%s): got %s, expected %s", d, got, expected) + } + } +} + +func TestDecimal_Ceil(t *testing.T) { + type testData struct { + input string + expected string + } + tests := []testData{ + {"1.999", "2"}, + {"1", "1"}, + {"1.01", "2"}, + {"0", "0"}, + {"0.9", "1"}, + {"0.1", "1"}, + {"-0.9", "0"}, + {"-0.1", "0"}, + {"-1.00", "-1"}, + {"-1.01", "-1"}, + {"-1.999", "-1"}, + } + for _, test := range tests { + d, _ := NewFromString(test.input) + expected, _ := NewFromString(test.expected) + got := d.Ceil() + if !got.Equals(expected) { + t.Errorf("Ceil(%s): got %s, expected %s", d, got, expected) + } + } +} + +func TestDecimal_RoundAndStringFixed(t *testing.T) { + type testData struct { + input string + places int32 + expected string + expectedFixed string + } + tests := []testData{ + {"1.454", 0, "1", ""}, + {"1.454", 1, "1.5", ""}, + {"1.454", 2, "1.45", ""}, + {"1.454", 3, "1.454", ""}, + {"1.454", 4, "1.454", "1.4540"}, + {"1.454", 5, "1.454", "1.45400"}, + {"1.554", 0, "2", ""}, + {"1.554", 1, "1.6", ""}, + {"1.554", 2, "1.55", ""}, + {"0.554", 0, "1", ""}, + {"0.454", 0, "0", ""}, + {"0.454", 5, "0.454", "0.45400"}, + {"0", 0, "0", ""}, + {"0", 1, "0", "0.0"}, + {"0", 2, "0", "0.00"}, + {"0", -1, "0", ""}, + {"5", 2, "5", "5.00"}, + {"5", 1, "5", "5.0"}, + {"5", 0, "5", ""}, + {"500", 2, "500", "500.00"}, + {"545", -1, "550", ""}, + {"545", -2, "500", ""}, + {"545", -3, "1000", ""}, + {"545", -4, "0", ""}, + {"499", -3, "0", ""}, + {"499", -4, "0", ""}, + } + + // add negative number tests + for _, test := range tests { + expected := test.expected + if expected != "0" { + expected = "-" + expected + } + expectedStr := test.expectedFixed + if strings.ContainsAny(expectedStr, "123456789") && expectedStr != "" { + expectedStr = "-" + expectedStr + } + tests = append(tests, + testData{"-" + test.input, test.places, expected, expectedStr}) + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + panic(err) + } + + // test Round + expected, err := NewFromString(test.expected) + if err != nil { + panic(err) + } + got := d.Round(test.places) + if !got.Equals(expected) { + t.Errorf("Rounding %s to %d places, got %s, expected %s", + d, test.places, got, expected) + } + + // test StringFixed + if test.expectedFixed == "" { + test.expectedFixed = test.expected + } + gotStr := d.StringFixed(test.places) + if gotStr != test.expectedFixed { + t.Errorf("(%s).StringFixed(%d): got %s, expected %s", + d, test.places, gotStr, test.expectedFixed) + } + } +} + func TestDecimal_Uninitialized(t *testing.T) { a := Decimal{} b := Decimal{}