Skip to content

🧧 Fixed-Point Decimal Money

License

Notifications You must be signed in to change notification settings

nikolaydubina/fpmoney

Repository files navigation

🧧 Fixed-Point Decimal Money

codecov Go Report Card Go Reference Mentioned in Awesome Go OpenSSF Scorecard

Be Precise: using floats to represent currency is almost criminal. — Robert.C.Martin, "Clean Code" p.301

  • as fast as int64
  • no float in parsing nor printing, does not leak precision
  • ISO 421712 currency
  • block mismatched currency arithmetics
  • 100 LOC
  • fuzz tests
var BuySP500Price = fpmoney.FromInt(9000, fpmoney.SGD)

input := []byte(`{"sp500": {"amount": 9000.02, "currency": "SGD"}}`)

type Stonks struct {
    SP500 fpmoney.Amount `json:"sp500"`
}
var v Stonks
if err := json.Unmarshal(input, &v); err != nil {
    log.Fatal(err)
}

amountToBuy := fpmoney.FromInt(0, fpmoney.SGD)
if v.SP500.GreaterThan(BuySP500Price) {
    amountToBuy = amountToBuy.Add(v.SP500.Mul(2))
}

fmt.Println(amountToBuy)
// Output: 18000.04 SGD

Ultra Small Fractions

Some denominations have very low fractions. Storing them int64 you would get.

  • BTC satoshi is 1 BTC = 100,000,000 satoshi, which is still enough for ~92,233,720,368 BTC.
  • ETH wei is 1 ETH = 1,000,000,000,000,000,000 wei, which is ~9 ETH. If you deal with wei, you may consider bigint or multiple int64. In fact, official Ethereum code is in Go and it is using bigint (code).

Benchmarks

$ go test -bench=. -benchmem .
goos: darwin
goarch: arm64
pkg: github.com/nikolaydubina/fpmoney
cpu: Apple M3 Max
BenchmarkCurrency_UnmarshalText-16      510934713                2.213 ns/op           0 B/op          0 allocs/op
BenchmarkCurrency_AppendText-16         439866170                2.714 ns/op           0 B/op          0 allocs/op
BenchmarkCurrency_MarshalText-16        88133492                13.52 ns/op            8 B/op          1 allocs/op
BenchmarkCurrency_String-16             1000000000               1.078 ns/op           0 B/op          0 allocs/op
BenchmarkArithmetic/add-16              901921378                1.562 ns/op           0 B/op          0 allocs/op
BenchmarkJSON/small/encode-16            5652006               211.6 ns/op           160 B/op          3 allocs/op
BenchmarkJSON/small/decode-16            4993570               236.0 ns/op           152 B/op          2 allocs/op
BenchmarkJSON/large/encode-16            4835323               246.9 ns/op           176 B/op          3 allocs/op
BenchmarkJSON/large/decode-16            3946946               304.9 ns/op           152 B/op          2 allocs/op
PASS
ok      github.com/nikolaydubina/fpmoney        11.287s

References and Related Work

  • ferdypruis/iso4217 was a good inspiration and reference material. it was used in early version as well. it is well maintained and fast library for currencies.
  • github.com/shopspring/decimal: fixed precision; faster printing/parsing/arithmetics; currency handling
  • github.com/Rhymond/go-money: does not use float or interface{} in parsing; currency is enum
  • github.com/ferdypruis/iso4217: skipped deprecated currencies to fit into uint8 and smaller struct size
  • https://en.wikipedia.org/wiki/ISO_4217

Footnotes

  1. excluding currencies with 4+ minor units CLF, UYW ↩

  2. excluding deprecated currencies HRD, HRK, SLL, ZWL ↩