-
Notifications
You must be signed in to change notification settings - Fork 1
/
amount.go
168 lines (135 loc) · 4.41 KB
/
amount.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package fpmoney
import (
"errors"
"github.com/nikolaydubina/fpdecimal"
)
// Amount stores fixed-precision decimal money.
// Stores integer number of cents for ISO 4217 currency.
// Values fit in ~92 quadrillion for 2 decimal currencies.
// Does not use float in printing nor parsing.
// Rounds down fractional cents during parsing.
// Blocking arithmetic operations that result in loss of precision.
type Amount struct {
v int64
c Currency
_ uint8 // padding for improved line alignment, 2x improves arithmetics
_ uint32 // padding
}
func FromIntScaled[T ~int | ~int8 | ~int16 | ~int32 | ~int64](v T, currency Currency) Amount {
return Amount{v: int64(v), c: currency}
}
func FromInt[T ~int | ~int8 | ~int16 | ~int32 | ~int64](v T, currency Currency) Amount {
return Amount{v: int64(v) * int64(currency.scale()), c: currency}
}
func FromFloat[T ~float32 | ~float64](v T, currency Currency) Amount {
return Amount{v: int64(T(v) * T(currency.scale())), c: currency}
}
func (a Amount) Float32() float32 { return float32(a.v) / float32(a.c.scale()) }
func (a Amount) Float64() float64 { return float64(a.v) / float64(a.c.scale()) }
func (a Amount) Currency() Currency { return a.c }
func (a Amount) Scaled() int64 { return a.v }
func (a Amount) Add(b Amount) Amount {
checkCurrency(a.c, b.c)
return Amount{v: a.v + b.v, c: a.c}
}
func (a Amount) Sub(b Amount) Amount {
checkCurrency(a.c, b.c)
return Amount{v: a.v - b.v, c: a.c}
}
func (a Amount) GreaterThan(b Amount) bool {
checkCurrency(a.c, b.c)
return a.v > b.v
}
func (a Amount) LessThan(b Amount) bool {
checkCurrency(a.c, b.c)
return a.v < b.v
}
func (a Amount) GreaterThanOrEqual(b Amount) bool {
checkCurrency(a.c, b.c)
return a.v >= b.v
}
func (a Amount) LessThanOrEqual(b Amount) bool {
checkCurrency(a.c, b.c)
return a.v <= b.v
}
func checkCurrency(a, b Currency) {
if a != b {
panic(&ErrCurrencyMismatch{A: a, B: b})
}
}
func (a Amount) Mul(b int) Amount { return Amount{v: a.v * int64(b), c: a.c} }
func (a Amount) DivMod(b int) (part Amount, remainder Amount) {
return Amount{v: a.v / int64(b), c: a.c}, Amount{v: a.v % int64(b), c: a.c}
}
func (a Amount) String() string {
return fpdecimal.FixedPointDecimalToString(a.v, a.c.Exponent()) + " " + a.c.String()
}
const (
keyCurrency = "currency"
keyAmount = "amount"
lenISO427Currency = 3
)
// UnmarshalJSON parses string.
// This is implemented directly for speed.
// Avoiding json.Decoder, interface{}, reflect, tags, temporary structs.
// Avoiding mallocs.
// Go json package provides:
// - check that pointer method receiver is not nil;
// - removes whitespace in b []bytes
func (a *Amount) UnmarshalJSON(b []byte) (err error) {
var as, ae, e int
for i := 0; i < len(b); i++ {
// currency
if b[i] == keyCurrency[0] && (i+len(keyCurrency)) <= len(b) && string(b[i:i+len(keyCurrency)]) == keyCurrency {
i += len(keyCurrency) + 2 // `":`
// find opening quote.
for ; i < len(b) && b[i] != '"'; i++ {
}
if i == len(b) {
return ErrWrongCurrencyString
}
i++ // opening `"`
e = i + lenISO427Currency
if e > len(b) {
return ErrWrongCurrencyString
}
if err := a.c.UnmarshalText(b[i:e]); err != nil {
return err
}
i = e
}
// amount
if b[i] == keyAmount[0] && (i+len(keyCurrency)) <= len(b) && string(b[i:i+len(keyAmount)]) == keyAmount {
i += len(keyAmount) + 2 // `":`
// go until find either number or + or -, which is a start of simple number.
for ; i < len(b) && !(b[i] >= '0' && b[i] <= '9') && b[i] != '-' && b[i] != '+'; i++ {
}
as = i
// find end of number
for ae = i; ae < len(b) && ((b[ae] >= '0' && b[ae] <= '9') || b[ae] == '-' || b[ae] == '+' || b[ae] == '.'); ae++ {
}
i = ae
}
}
a.v, err = fpdecimal.ParseFixedPointDecimal(b[as:ae], a.c.Exponent())
return err
}
func (a Amount) MarshalJSON() ([]byte, error) {
b := make([]byte, 0, 100)
b = append(b, `{"`...)
b = append(b, keyAmount...)
b = append(b, `":`...)
b = fpdecimal.AppendFixedPointDecimal(b, a.v, a.c.Exponent())
b = append(b, `,"`...)
b = append(b, keyCurrency...)
b = append(b, `":"`...)
b = append(b, a.c.String()...)
b = append(b, `"}`...)
return b, nil
}
var ErrWrongCurrencyString = errors.New("wrong currency string")
type ErrCurrencyMismatch struct {
A, B Currency
}
func NewErrCurrencyMismatch() *ErrCurrencyMismatch { return &ErrCurrencyMismatch{} }
func (e *ErrCurrencyMismatch) Error() string { return e.A.String() + " != " + e.B.String() }