From aed1bfe463fa3c9cc268d60dcc1491db613bff7e Mon Sep 17 00:00:00 2001 From: Rigel Date: Mon, 11 Sep 2017 08:58:34 -0400 Subject: [PATCH] Added Banker Rounding (#61) * made GTE/GT/LT/LTE go-lint compliant * added banker rounding and tests (RoundBank, StringFixedBank) --- decimal.go | 57 ++++++++++++++++++++++++++++++--- decimal_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 4 deletions(-) diff --git a/decimal.go b/decimal.go index 17c8d72..1729b61 100644 --- a/decimal.go +++ b/decimal.go @@ -431,23 +431,23 @@ func (d Decimal) Equals(d2 Decimal) bool { return d.Equal(d2) } -// Greater Than (GT) returns true when d is greater than d2. +// GreaterThan (GT) returns true when d is greater than d2. func (d Decimal) GreaterThan(d2 Decimal) bool { return d.Cmp(d2) == 1 } -// Greater Than or Equal (GTE) returns true when d is greater than or equal to d2. +// GreaterThanOrEqual (GTE) returns true when d is greater than or equal to d2. func (d Decimal) GreaterThanOrEqual(d2 Decimal) bool { cmp := d.Cmp(d2) return cmp == 1 || cmp == 0 } -// Less Than (LT) returns true when d is less than d2. +// LessThan (LT) returns true when d is less than d2. func (d Decimal) LessThan(d2 Decimal) bool { return d.Cmp(d2) == -1 } -// Less Than or Equal (LTE) returns true when d is less than or equal to d2. +// LessThanOrEqual (LTE) returns true when d is less than or equal to d2. func (d Decimal) LessThanOrEqual(d2 Decimal) bool { cmp := d.Cmp(d2) return cmp == -1 || cmp == 0 @@ -539,6 +539,24 @@ func (d Decimal) StringFixed(places int32) string { return rounded.string(false) } +// StringFixedBank returns a banker 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.4" +// 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) StringFixedBank(places int32) string { + rounded := d.RoundBank(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). // @@ -568,6 +586,37 @@ func (d Decimal) Round(places int32) Decimal { return ret } +// RoundBank rounds the decimal to places decimal places. +// If the final digit to round is equidistant from the nearest two integers the +// rounded value is taken as the even number +// +// If places < 0, it will round the integer part to the nearest 10^(-places). +// +// Examples: +// +// NewFromFloat(5.45).Round(1).String() // output: "5.4" +// NewFromFloat(545).Round(-1).String() // output: "540" +// NewFromFloat(5.46).Round(1).String() // output: "5.5" +// NewFromFloat(546).Round(-1).String() // output: "550" +// NewFromFloat(5.55).Round(1).String() // output: "5.6" +// NewFromFloat(555).Round(-1).String() // output: "560" +// +func (d Decimal) RoundBank(places int32) Decimal { + + round := d.Round(places) + remainder := d.Sub(round).Abs() + + if remainder.value.Cmp(fiveInt) == 0 && round.value.Bit(0) != 0 { + if round.value.Sign() < 0 { + round.value.Add(round.value, oneInt) + } else { + round.value.Sub(round.value, oneInt) + } + } + + return round +} + // Floor returns the nearest integer value less than or equal to d. func (d Decimal) Floor() Decimal { d.ensureInitialized() diff --git a/decimal_test.go b/decimal_test.go index fc9cb12..d606dc9 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -550,6 +550,90 @@ func TestDecimal_RoundAndStringFixed(t *testing.T) { } } +func TestDecimal_BankRoundAndStringFixed(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", -2, "500", ""}, + {"545", -3, "1000", ""}, + {"545", -4, "0", ""}, + {"499", -3, "0", ""}, + {"499", -4, "0", ""}, + {"1.45", 1, "1.4", ""}, + {"1.55", 1, "1.6", ""}, + {"1.65", 1, "1.6", ""}, + {"545", -1, "540", ""}, + {"565", -1, "560", ""}, + {"555", -1, "560", ""}, + } + + // 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.RoundBank(test.places) + if !got.Equal(expected) { + t.Errorf("Bank 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.StringFixedBank(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{}