Skip to content

Commit

Permalink
Implement Swedish/Cash rounding fixes #63
Browse files Browse the repository at this point in the history
Performance and allocations not that good but can be optimized later.

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

BenchmarkDecimal_RoundCash/five-4         	 1000000	      1918 ns/op	    1164 B/op	      30 allocs/op
BenchmarkDecimal_RoundCash/fifteen-4      	  300000	      4331 ns/op	    2940 B/op	      74 allocs/op
  • Loading branch information
SchumacherFM committed Oct 19, 2017
1 parent ad668bb commit 78079cf
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 64 deletions.
61 changes: 61 additions & 0 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
//
Expand Down Expand Up @@ -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()
Expand Down
229 changes: 165 additions & 64 deletions decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,13 @@ func TestNewFromFloatWithExponent(t *testing.T) {
exp int32
}
tests := map[Inp]string{
Inp{123.4, -3}: "123.4",
Inp{123.4, -1}: "123.4",
Inp{123.412345, 1}: "120",
Inp{123.412345, 0}: "123",
Inp{123.412345, -5}: "123.41235",
Inp{123.412345, -6}: "123.412345",
Inp{123.412345, -7}: "123.412345",
{123.4, -3}: "123.4",
{123.4, -1}: "123.4",
{123.412345, 1}: "120",
{123.412345, 0}: "123",
{123.412345, -5}: "123.41235",
{123.412345, -6}: "123.412345",
{123.412345, -7}: "123.412345",
}

// add negatives
Expand Down Expand Up @@ -237,13 +237,13 @@ func TestNewFromBigIntWithExponent(t *testing.T) {
exp int32
}
tests := map[Inp]string{
Inp{big.NewInt(123412345), -3}: "123412.345",
Inp{big.NewInt(2234), -1}: "223.4",
Inp{big.NewInt(323412345), 1}: "3234123450",
Inp{big.NewInt(423412345), 0}: "423412345",
Inp{big.NewInt(52341235), -5}: "523.41235",
Inp{big.NewInt(623412345), -6}: "623.412345",
Inp{big.NewInt(723412345), -7}: "72.3412345",
{big.NewInt(123412345), -3}: "123412.345",
{big.NewInt(2234), -1}: "223.4",
{big.NewInt(323412345), 1}: "3234123450",
{big.NewInt(423412345), 0}: "423412345",
{big.NewInt(52341235), -5}: "523.41235",
{big.NewInt(623412345), -6}: "623.412345",
{big.NewInt(723412345), -7}: "72.3412345",
}

// add negatives
Expand Down Expand Up @@ -477,10 +477,10 @@ func TestDecimal_rescale(t *testing.T) {
rescale int32
}
tests := map[Inp]string{
Inp{1234, -3, -5}: "1.234",
Inp{1234, -3, 0}: "1",
Inp{1234, 3, 0}: "1234000",
Inp{1234, -4, -4}: "0.1234",
{1234, -3, -5}: "1.234",
{1234, -3, 0}: "1",
{1234, 3, 0}: "1234000",
{1234, -4, -4}: "0.1234",
}

// add negatives
Expand Down Expand Up @@ -784,12 +784,12 @@ func TestDecimal_Add(t *testing.T) {
}

inputs := map[Inp]string{
Inp{"2", "3"}: "5",
Inp{"2454495034", "3451204593"}: "5905699627",
Inp{"24544.95034", ".3451204593"}: "24545.2954604593",
Inp{".1", ".1"}: "0.2",
Inp{".1", "-.1"}: "0",
Inp{"0", "1.001"}: "1.001",
{"2", "3"}: "5",
{"2454495034", "3451204593"}: "5905699627",
{"24544.95034", ".3451204593"}: "24545.2954604593",
{".1", ".1"}: "0.2",
{".1", "-.1"}: "0",
{"0", "1.001"}: "1.001",
}

for inp, res := range inputs {
Expand All @@ -815,16 +815,16 @@ func TestDecimal_Sub(t *testing.T) {
}

inputs := map[Inp]string{
Inp{"2", "3"}: "-1",
Inp{"12", "3"}: "9",
Inp{"-2", "9"}: "-11",
Inp{"2454495034", "3451204593"}: "-996709559",
Inp{"24544.95034", ".3451204593"}: "24544.6052195407",
Inp{".1", "-.1"}: "0.2",
Inp{".1", ".1"}: "0",
Inp{"0", "1.001"}: "-1.001",
Inp{"1.001", "0"}: "1.001",
Inp{"2.3", ".3"}: "2",
{"2", "3"}: "-1",
{"12", "3"}: "9",
{"-2", "9"}: "-11",
{"2454495034", "3451204593"}: "-996709559",
{"24544.95034", ".3451204593"}: "24544.6052195407",
{".1", "-.1"}: "0.2",
{".1", ".1"}: "0",
{"0", "1.001"}: "-1.001",
{"1.001", "0"}: "1.001",
{"2.3", ".3"}: "2",
}

for inp, res := range inputs {
Expand Down Expand Up @@ -871,11 +871,11 @@ func TestDecimal_Mul(t *testing.T) {
}

inputs := map[Inp]string{
Inp{"2", "3"}: "6",
Inp{"2454495034", "3451204593"}: "8470964534836491162",
Inp{"24544.95034", ".3451204593"}: "8470.964534836491162",
Inp{".1", ".1"}: "0.01",
Inp{"0", "1.001"}: "0",
{"2", "3"}: "6",
{"2454495034", "3451204593"}: "8470964534836491162",
{"24544.95034", ".3451204593"}: "8470.964534836491162",
{".1", ".1"}: "0.01",
{"0", "1.001"}: "0",
}

for inp, res := range inputs {
Expand Down Expand Up @@ -907,18 +907,18 @@ func TestDecimal_Div(t *testing.T) {
}

inputs := map[Inp]string{
Inp{"6", "3"}: "2",
Inp{"10", "2"}: "5",
Inp{"2.2", "1.1"}: "2",
Inp{"-2.2", "-1.1"}: "2",
Inp{"12.88", "5.6"}: "2.3",
Inp{"1023427554493", "43432632"}: "23563.5628642767953828", // rounded
Inp{"1", "434324545566634"}: "0.0000000000000023",
Inp{"1", "3"}: "0.3333333333333333",
Inp{"2", "3"}: "0.6666666666666667", // rounded
Inp{"10000", "3"}: "3333.3333333333333333",
Inp{"10234274355545544493", "-3"}: "-3411424785181848164.3333333333333333",
Inp{"-4612301402398.4753343454", "23.5"}: "-196268144782.9138440146978723",
{"6", "3"}: "2",
{"10", "2"}: "5",
{"2.2", "1.1"}: "2",
{"-2.2", "-1.1"}: "2",
{"12.88", "5.6"}: "2.3",
{"1023427554493", "43432632"}: "23563.5628642767953828", // rounded
{"1", "434324545566634"}: "0.0000000000000023",
{"1", "3"}: "0.3333333333333333",
{"2", "3"}: "0.6666666666666667", // rounded
{"10000", "3"}: "3333.3333333333333333",
{"10234274355545544493", "-3"}: "-3411424785181848164.3333333333333333",
{"-4612301402398.4753343454", "23.5"}: "-196268144782.9138440146978723",
}

for inp, expectedStr := range inputs {
Expand Down Expand Up @@ -951,11 +951,11 @@ func TestDecimal_Div(t *testing.T) {

// test code path where exp > 0
inputs2 := map[Inp2]string{
Inp2{124, 10, 3, 1}: "41333333333.3333333333333333",
Inp2{124, 10, 3, 0}: "413333333333.3333333333333333",
Inp2{124, 10, 6, 1}: "20666666666.6666666666666667",
Inp2{124, 10, 6, 0}: "206666666666.6666666666666667",
Inp2{10, 10, 10, 1}: "1000000000",
{124, 10, 3, 1}: "41333333333.3333333333333333",
{124, 10, 3, 0}: "413333333333.3333333333333333",
{124, 10, 6, 1}: "20666666666.6666666666666667",
{124, 10, 6, 0}: "206666666666.6666666666666667",
{10, 10, 10, 1}: "1000000000",
}

for inp, expectedAbs := range inputs2 {
Expand Down Expand Up @@ -1231,21 +1231,122 @@ 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(b *testing.B) {
b.Run("five", func(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)
}
}
})
b.Run("fifteen", func(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
b string
}

inputs := map[Inp]string{
Inp{"3", "2"}: "1",
Inp{"3451204593", "2454495034"}: "996709559",
Inp{"24544.95034", ".3451204593"}: "0.3283950433",
Inp{".1", ".1"}: "0",
Inp{"0", "1.001"}: "0",
Inp{"-7.5", "2"}: "-1.5",
Inp{"7.5", "-2"}: "1.5",
Inp{"-7.5", "-2"}: "-1.5",
{"3", "2"}: "1",
{"3451204593", "2454495034"}: "996709559",
{"24544.95034", ".3451204593"}: "0.3283950433",
{".1", ".1"}: "0",
{"0", "1.001"}: "0",
{"-7.5", "2"}: "-1.5",
{"7.5", "-2"}: "1.5",
{"-7.5", "-2"}: "-1.5",
}

for inp, res := range inputs {
Expand Down

0 comments on commit 78079cf

Please sign in to comment.