mirror of
https://github.com/shopspring/decimal.git
synced 2024-11-22 20:40:48 +01:00
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
This commit is contained in:
parent
ad668bb369
commit
faaed5fca7
2 changed files with 161 additions and 0 deletions
61
decimal.go
61
decimal.go
|
@ -55,10 +55,16 @@ var MarshalJSONWithoutQuotes = false
|
||||||
// Zero constant, to make computations faster.
|
// Zero constant, to make computations faster.
|
||||||
var Zero = New(0, 1)
|
var Zero = New(0, 1)
|
||||||
|
|
||||||
|
// fiveDec used in Cash Rounding
|
||||||
|
var fiveDec = New(5, 0)
|
||||||
|
|
||||||
var zeroInt = big.NewInt(0)
|
var zeroInt = big.NewInt(0)
|
||||||
var oneInt = big.NewInt(1)
|
var oneInt = big.NewInt(1)
|
||||||
|
var twoInt = big.NewInt(2)
|
||||||
|
var fourInt = big.NewInt(4)
|
||||||
var fiveInt = big.NewInt(5)
|
var fiveInt = big.NewInt(5)
|
||||||
var tenInt = big.NewInt(10)
|
var tenInt = big.NewInt(10)
|
||||||
|
var twentyInt = big.NewInt(20)
|
||||||
|
|
||||||
// Decimal represents a fixed-point decimal. It is immutable.
|
// Decimal represents a fixed-point decimal. It is immutable.
|
||||||
// number = value * 10 ^ exp
|
// number = value * 10 ^ exp
|
||||||
|
@ -557,6 +563,13 @@ func (d Decimal) StringFixedBank(places int32) string {
|
||||||
return rounded.string(false)
|
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.
|
// Round rounds the decimal to places decimal places.
|
||||||
// If places < 0, it will round the integer part to the nearest 10^(-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
|
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.
|
// Floor returns the nearest integer value less than or equal to d.
|
||||||
func (d Decimal) Floor() Decimal {
|
func (d Decimal) Floor() Decimal {
|
||||||
d.ensureInitialized()
|
d.ensureInitialized()
|
||||||
|
|
100
decimal_test.go
100
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) {
|
func TestDecimal_Mod(t *testing.T) {
|
||||||
type Inp struct {
|
type Inp struct {
|
||||||
a string
|
a string
|
||||||
|
|
Loading…
Reference in a new issue