aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCyrill Schumacher <Cyrill@Schumacher.fm>2017-10-20 22:00:25 +0800
committerVictor Quinn <mail@victorquinn.com>2017-10-20 22:00:25 +0800
commitfaaed5fca71709f7bb4e0b0e564089c7a4a1b3a8 (patch)
tree9162fe637c0b597c6edb2f6ad67cccd2a271267e
parentad668bb36961e1e1ac005e514b610e41573603bf (diff)
downloaddexon-decimal-faaed5fca71709f7bb4e0b0e564089c7a4a1b3a8.tar.gz
dexon-decimal-faaed5fca71709f7bb4e0b0e564089c7a4a1b3a8.tar.zst
dexon-decimal-faaed5fca71709f7bb4e0b0e564089c7a4a1b3a8.zip
Implement Swedish/Cash rounding fixes #63 (#66)
Performance and allocations not that good but can be optimized later. RoundSwedish aka Cash/Penny/öre rounding rounds decimal to a specific interval. The amount payable for a cash transaction is rounded to the nearest multiple of the minimum currency unit available. The following intervals are available: 5, 10, 15, 25, 50 and 100; any other number throws a panic. 5: 5 cent rounding 3.43 => 3.45 10: 10 cent rounding 3.45 => 3.50 (5 gets rounded up) 15: 10 cent rounding 3.45 => 3.40 (5 gets rounded down) 25: 25 cent rounding 3.41 => 3.50 50: 50 cent rounding 3.75 => 4.00 100: 100 cent rounding 3.50 => 4.00 For more details: https://en.wikipedia.org/wiki/Cash_rounding BenchmarkDecimal_RoundSwedish/five-4 1000000 1918 ns/op 1164 B/op 30 allocs/op BenchmarkDecimal_RoundSwedish/fifteen-4 300000 4331 ns/op 2940 B/op 74 allocs/op
-rw-r--r--decimal.go61
-rw-r--r--decimal_test.go100
2 files changed, 161 insertions, 0 deletions
diff --git a/decimal.go b/decimal.go
index 6078361..a78e390 100644
--- a/decimal.go
+++ b/decimal.go
@@ -55,10 +55,16 @@ var MarshalJSONWithoutQuotes = false
// Zero constant, to make computations faster.
var Zero = New(0, 1)
+// fiveDec used in Cash Rounding
+var fiveDec = New(5, 0)
+
var zeroInt = big.NewInt(0)
var oneInt = big.NewInt(1)
+var twoInt = big.NewInt(2)
+var fourInt = big.NewInt(4)
var fiveInt = big.NewInt(5)
var tenInt = big.NewInt(10)
+var twentyInt = big.NewInt(20)
// Decimal represents a fixed-point decimal. It is immutable.
// number = value * 10 ^ exp
@@ -557,6 +563,13 @@ func (d Decimal) StringFixedBank(places int32) string {
return rounded.string(false)
}
+// StringFixedCash returns a Swedish/Cash rounded fixed-point string. For
+// more details see the documentation at function RoundCash.
+func (d Decimal) StringFixedCash(interval uint8) string {
+ rounded := d.RoundCash(interval)
+ 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).
//
@@ -617,6 +630,54 @@ func (d Decimal) RoundBank(places int32) Decimal {
return round
}
+// RoundCash aka Cash/Penny/öre rounding rounds decimal to a specific
+// interval. The amount payable for a cash transaction is rounded to the nearest
+// multiple of the minimum currency unit available. The following intervals are
+// available: 5, 10, 15, 25, 50 and 100; any other number throws a panic.
+// 5: 5 cent rounding 3.43 => 3.45
+// 10: 10 cent rounding 3.45 => 3.50 (5 gets rounded up)
+// 15: 10 cent rounding 3.45 => 3.40 (5 gets rounded down)
+// 25: 25 cent rounding 3.41 => 3.50
+// 50: 50 cent rounding 3.75 => 4.00
+// 100: 100 cent rounding 3.50 => 4.00
+// For more details: https://en.wikipedia.org/wiki/Cash_rounding
+func (d Decimal) RoundCash(interval uint8) Decimal {
+ var iVal *big.Int
+ switch interval {
+ case 5:
+ iVal = twentyInt
+ case 10:
+ iVal = tenInt
+ case 15:
+ if d.exp < 0 {
+ // TODO: optimize and reduce allocations
+ orgExp := d.exp
+ dOne := New(10^-int64(orgExp), orgExp)
+ d2 := d
+ d2.exp = 0
+ if d2.Mod(fiveDec).Equal(Zero) {
+ d2.exp = orgExp
+ d2 = d2.Sub(dOne)
+ d = d2
+ }
+ }
+ iVal = tenInt
+ case 25:
+ iVal = fourInt
+ case 50:
+ iVal = twoInt
+ case 100:
+ iVal = oneInt
+ default:
+ panic(fmt.Sprintf("Decimal does not support this Cash rounding interval `%d`. Supported: 5, 10, 15, 25, 50, 100", interval))
+ }
+ dVal := Decimal{
+ value: iVal,
+ }
+ // TODO: optimize those calculations to reduce the high allocations (~29 allocs).
+ return d.Mul(dVal).Round(0).Div(dVal).Truncate(2)
+}
+
// 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 9786bcc..8476bed 100644
--- a/decimal_test.go
+++ b/decimal_test.go
@@ -1231,6 +1231,106 @@ func TestDecimal_DivRound2(t *testing.T) {
}
}
+func TestDecimal_RoundCash(t *testing.T) {
+ tests := []struct {
+ d string
+ interval uint8
+ result string
+ }{
+ {"3.44", 5, "3.45"},
+ {"3.43", 5, "3.45"},
+ {"3.42", 5, "3.40"},
+ {"3.425", 5, "3.45"},
+ {"3.47", 5, "3.45"},
+ {"3.478", 5, "3.50"},
+ {"3.48", 5, "3.50"},
+ {"348", 5, "348"},
+
+ {"3.23", 10, "3.20"},
+ {"3.33", 10, "3.30"},
+ {"3.53", 10, "3.50"},
+ {"3.949", 10, "3.90"},
+ {"3.95", 10, "4.00"},
+ {"395", 10, "395"},
+
+ {"6.42", 15, "6.40"},
+ {"6.39", 15, "6.40"},
+ {"6.35", 15, "6.30"},
+ {"6.36", 15, "6.40"},
+ {"6.349", 15, "6.30"},
+ {"6.30", 15, "6.30"},
+ {"666", 15, "666"},
+
+ {"3.23", 25, "3.25"},
+ {"3.33", 25, "3.25"},
+ {"3.53", 25, "3.50"},
+ {"3.93", 25, "4.00"},
+ {"3.41", 25, "3.50"},
+
+ {"3.249", 50, "3.00"},
+ {"3.33", 50, "3.50"},
+ {"3.749999999", 50, "3.50"},
+ {"3.75", 50, "4.00"},
+ {"3.93", 50, "4.00"},
+ {"393", 50, "393"},
+
+ {"3.249", 100, "3.00"},
+ {"3.49999", 100, "3.00"},
+ {"3.50", 100, "4.00"},
+ {"3.75", 100, "4.00"},
+ {"3.93", 100, "4.00"},
+ {"393", 100, "393"},
+ }
+ for i, test := range tests {
+ d, _ := NewFromString(test.d)
+ haveRounded := d.RoundCash(test.interval)
+ result, _ := NewFromString(test.result)
+
+ if !haveRounded.Equal(result) {
+ t.Errorf("Index %d: Cash rounding for %q interval %d want %q, have %q", i, test.d, test.interval, test.result, haveRounded)
+ }
+ }
+}
+
+func TestDecimal_RoundCash_Panic(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ if have, ok := r.(string); ok {
+ const want = "Decimal does not support this Cash rounding interval `231`. Supported: 5, 10, 15, 25, 50, 100"
+ if want != have {
+ t.Errorf("\nWant: %q\nHave: %q", want, have)
+ }
+ } else {
+ t.Errorf("Panic should contain an error string but got:\n%+v", r)
+ }
+ } else {
+ t.Error("Expecting a panic but got nothing")
+ }
+ }()
+ d, _ := NewFromString("1")
+ d.RoundCash(231)
+}
+
+func BenchmarkDecimal_RoundCash_Five(b *testing.B) {
+ const want = "3.50"
+ for i := 0; i < b.N; i++ {
+ val := New(3478, -3)
+ if have := val.StringFixedCash(5); have != want {
+ b.Fatalf("\nHave: %q\nWant: %q", have, want)
+ }
+ }
+}
+
+func BenchmarkDecimal_RoundCash_Fifteen(b *testing.B) {
+ const want = "6.30"
+ for i := 0; i < b.N; i++ {
+ val := New(635, -2)
+ if have := val.StringFixedCash(15); have != want {
+ b.Fatalf("\nHave: %q\nWant: %q", have, want)
+ }
+ }
+}
+
func TestDecimal_Mod(t *testing.T) {
type Inp struct {
a string