fpmoney

package module
v1.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 6, 2024 License: MIT Imports: 2 Imported by: 0

README ¶

🧧 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 4217 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 . > fpmoney.bench
$ go test -bench=. -benchmem ./internal/bench/float32 > float32.bench
$ go test -bench=. -benchmem ./internal/bench/int > int.bench
$ benchstat -split="XYZ" int.bench float32.bench fpmoney.bench
name \ time/op              int.bench   float32.bench  fpmoney.bench
JSONUnmarshal/small-16      383ns ± 0%     408ns ± 0%     294ns ± 0%
JSONUnmarshal/large-16      436ns ± 0%     473ns ± 0%     365ns ± 0%
JSONMarshal/small-16        115ns ± 0%     158ns ± 0%     226ns ± 0%
JSONMarshal/large-16        112ns ± 0%     146ns ± 0%     272ns ± 0%

name \ alloc/op             int.bench   float32.bench  fpmoney.bench
JSONUnmarshal/small-16       268B ± 0%      270B ± 0%      198B ± 0%
JSONUnmarshal/large-16       272B ± 0%      288B ± 0%      216B ± 0%
JSONMarshal/small-16        57.0B ± 0%     66.0B ± 0%    160.0B ± 0%
JSONMarshal/large-16        72.0B ± 0%     72.0B ± 0%    176.0B ± 0%

name \ allocs/op            int.bench   float32.bench  fpmoney.bench
JSONUnmarshal/small-16       6.00 ± 0%      6.00 ± 0%      3.00 ± 0%
JSONUnmarshal/large-16       6.00 ± 0%      6.00 ± 0%      3.00 ± 0%
JSONMarshal/small-16         2.00 ± 0%      2.00 ± 0%      3.00 ± 0%
JSONMarshal/large-16         2.00 ± 0%      2.00 ± 0%      3.00 ± 0%
goos: darwin
goarch: arm64
pkg: github.com/nikolaydubina/fpmoney
BenchmarkArithmetic/add_x1-16         1000000000	         0.54 ns/op	       0 B/op	       0 allocs/op
BenchmarkArithmetic/add_x100-16       	26382420	        44.42 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/nikolaydubina/fpmoney	14.200s
  • 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

Documentation ¶

Overview ¶

Code generated by go-enum-encoding DO NOT EDIT

Index ¶

Examples ¶

Constants ¶

This section is empty.

Variables ¶

View Source
var (
	AED = Currency{1}   // json:"AED"
	AFN = Currency{2}   // json:"AFN"
	ALL = Currency{3}   // json:"ALL"
	AMD = Currency{4}   // json:"AMD"
	ANG = Currency{5}   // json:"ANG"
	AOA = Currency{6}   // json:"AOA"
	ARS = Currency{7}   // json:"ARS"
	AUD = Currency{8}   // json:"AUD"
	AWG = Currency{9}   // json:"AWG"
	AZN = Currency{10}  // json:"AZN"
	BAM = Currency{11}  // json:"BAM"
	BBD = Currency{12}  // json:"BBD"
	BDT = Currency{13}  // json:"BDT"
	BGN = Currency{14}  // json:"BGN"
	BHD = Currency{15}  // json:"BHD"
	BIF = Currency{16}  // json:"BIF"
	BMD = Currency{17}  // json:"BMD"
	BND = Currency{18}  // json:"BND"
	BOB = Currency{19}  // json:"BOB"
	BOV = Currency{20}  // json:"BOV"
	BRL = Currency{21}  // json:"BRL"
	BSD = Currency{22}  // json:"BSD"
	BTN = Currency{23}  // json:"BTN"
	BWP = Currency{24}  // json:"BWP"
	BYN = Currency{25}  // json:"BYN"
	BZD = Currency{26}  // json:"BZD"
	CAD = Currency{27}  // json:"CAD"
	CDF = Currency{28}  // json:"CDF"
	CHE = Currency{29}  // json:"CHE"
	CHF = Currency{30}  // json:"CHF"
	CHW = Currency{31}  // json:"CHW"
	CLF = Currency{32}  // json:"CLF"
	CLP = Currency{33}  // json:"CLP"
	CNY = Currency{34}  // json:"CNY"
	COP = Currency{35}  // json:"COP"
	COU = Currency{36}  // json:"COU"
	CRC = Currency{37}  // json:"CRC"
	CUP = Currency{38}  // json:"CUP"
	CVE = Currency{39}  // json:"CVE"
	CZK = Currency{40}  // json:"CZK"
	DJF = Currency{41}  // json:"DJF"
	DKK = Currency{42}  // json:"DKK"
	DOP = Currency{43}  // json:"DOP"
	DZD = Currency{44}  // json:"DZD"
	EGP = Currency{45}  // json:"EGP"
	ERN = Currency{46}  // json:"ERN"
	ETB = Currency{47}  // json:"ETB"
	EUR = Currency{48}  // json:"EUR"
	FJD = Currency{49}  // json:"FJD"
	FKP = Currency{50}  // json:"FKP"
	GBP = Currency{51}  // json:"GBP"
	GEL = Currency{52}  // json:"GEL"
	GHS = Currency{53}  // json:"GHS"
	GIP = Currency{54}  // json:"GIP"
	GMD = Currency{55}  // json:"GMD"
	GNF = Currency{56}  // json:"GNF"
	GTQ = Currency{57}  // json:"GTQ"
	GYD = Currency{58}  // json:"GYD"
	HKD = Currency{59}  // json:"HKD"
	HNL = Currency{60}  // json:"HNL"
	HRD = Currency{61}  // json:"HRD"
	HRK = Currency{62}  // json:"HRK"
	HTG = Currency{63}  // json:"HTG"
	HUF = Currency{64}  // json:"HUF"
	IDR = Currency{65}  // json:"IDR"
	ILS = Currency{66}  // json:"ILS"
	INR = Currency{67}  // json:"INR"
	IQD = Currency{68}  // json:"IQD"
	IRR = Currency{69}  // json:"IRR"
	ISK = Currency{70}  // json:"ISK"
	JMD = Currency{71}  // json:"JMD"
	JOD = Currency{72}  // json:"JOD"
	JPY = Currency{73}  // json:"JPY"
	KES = Currency{74}  // json:"KES"
	KGS = Currency{75}  // json:"KGS"
	KHR = Currency{76}  // json:"KHR"
	KMF = Currency{77}  // json:"KMF"
	KPW = Currency{78}  // json:"KPW"
	KRW = Currency{79}  // json:"KRW"
	KWD = Currency{80}  // json:"KWD"
	KYD = Currency{81}  // json:"KYD"
	KZT = Currency{82}  // json:"KZT"
	LAK = Currency{83}  // json:"LAK"
	LBP = Currency{84}  // json:"LBP"
	LKR = Currency{85}  // json:"LKR"
	LRD = Currency{86}  // json:"LRD"
	LSL = Currency{87}  // json:"LSL"
	LYD = Currency{88}  // json:"LYD"
	MAD = Currency{89}  // json:"MAD"
	MDL = Currency{90}  // json:"MDL"
	MGA = Currency{91}  // json:"MGA"
	MKD = Currency{92}  // json:"MKD"
	MMK = Currency{93}  // json:"MMK"
	MNT = Currency{94}  // json:"MNT"
	MOP = Currency{95}  // json:"MOP"
	MRU = Currency{96}  // json:"MRU"
	MUR = Currency{97}  // json:"MUR"
	MVR = Currency{98}  // json:"MVR"
	MWK = Currency{99}  // json:"MWK"
	MXN = Currency{100} // json:"MXN"
	MXV = Currency{101} // json:"MXV"
	MYR = Currency{102} // json:"MYR"
	MZN = Currency{103} // json:"MZN"
	NAD = Currency{104} // json:"NAD"
	NGN = Currency{105} // json:"NGN"
	NIO = Currency{106} // json:"NIO"
	NOK = Currency{107} // json:"NOK"
	NPR = Currency{108} // json:"NPR"
	NZD = Currency{109} // json:"NZD"
	OMR = Currency{110} // json:"OMR"
	PAB = Currency{111} // json:"PAB"
	PEN = Currency{112} // json:"PEN"
	PGK = Currency{113} // json:"PGK"
	PHP = Currency{114} // json:"PHP"
	PKR = Currency{115} // json:"PKR"
	PLN = Currency{116} // json:"PLN"
	PYG = Currency{117} // json:"PYG"
	QAR = Currency{118} // json:"QAR"
	RON = Currency{119} // json:"RON"
	RSD = Currency{120} // json:"RSD"
	RUB = Currency{121} // json:"RUB"
	RWF = Currency{122} // json:"RWF"
	SAR = Currency{123} // json:"SAR"
	SBD = Currency{124} // json:"SBD"
	SCR = Currency{125} // json:"SCR"
	SDG = Currency{126} // json:"SDG"
	SEK = Currency{127} // json:"SEK"
	SGD = Currency{128} // json:"SGD"
	SHP = Currency{129} // json:"SHP"
	SLE = Currency{130} // json:"SLE"
	SLL = Currency{131} // json:"SLL"
	SOS = Currency{132} // json:"SOS"
	SRD = Currency{133} // json:"SRD"
	SSP = Currency{134} // json:"SSP"
	STN = Currency{135} // json:"STN"
	SVC = Currency{136} // json:"SVC"
	SYP = Currency{137} // json:"SYP"
	SZL = Currency{138} // json:"SZL"
	THB = Currency{139} // json:"THB"
	TJS = Currency{140} // json:"TJS"
	TMT = Currency{141} // json:"TMT"
	TND = Currency{142} // json:"TND"
	TOP = Currency{143} // json:"TOP"
	TRY = Currency{144} // json:"TRY"
	TTD = Currency{145} // json:"TTD"
	TWD = Currency{146} // json:"TWD"
	TZS = Currency{147} // json:"TZS"
	UAH = Currency{148} // json:"UAH"
	UGX = Currency{149} // json:"UGX"
	USD = Currency{150} // json:"USD"
	USN = Currency{151} // json:"USN"
	UYI = Currency{152} // json:"UYI"
	UYU = Currency{153} // json:"UYU"
	UYW = Currency{154} // json:"UYW"
	UZS = Currency{155} // json:"UZS"
	VED = Currency{156} // json:"VED"
	VES = Currency{157} // json:"VES"
	VND = Currency{158} // json:"VND"
	VUV = Currency{159} // json:"VUV"
	WST = Currency{160} // json:"WST"
	XAF = Currency{161} // json:"XAF"
	XAG = Currency{162} // json:"XAG"
	XAU = Currency{163} // json:"XAU"
	XBA = Currency{164} // json:"XBA"
	XBB = Currency{165} // json:"XBB"
	XBC = Currency{166} // json:"XBC"
	XBD = Currency{167} // json:"XBD"
	XCD = Currency{168} // json:"XCD"
	XDR = Currency{169} // json:"XDR"
	XOF = Currency{170} // json:"XOF"
	XPD = Currency{171} // json:"XPD"
	XPF = Currency{172} // json:"XPF"
	XPT = Currency{173} // json:"XPT"
	XSU = Currency{174} // json:"XSU"
	XTS = Currency{175} // json:"XTS"
	XUA = Currency{176} // json:"XUA"
	XXX = Currency{177} // json:"XXX"
	YER = Currency{178} // json:"YER"
	ZAR = Currency{179} // json:"ZAR"
	ZMW = Currency{180} // json:"ZMW"
	ZWL = Currency{181} // json:"ZWL"
)
View Source
var ErrUnknownCurrency = errors.New("unknown Currency")

Functions ¶

This section is empty.

Types ¶

type Amount ¶

type Amount struct {
	// contains filtered or unexported fields
}

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.

Example ¶
package main

import (
	"encoding/json"
	"fmt"
	"log"

	_ "embed"
	"github.com/nikolaydubina/fpmoney"
)

func main() {
	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
Example (Equality) ¶
package main

import (
	"fmt"

	_ "embed"
	"github.com/nikolaydubina/fpmoney"
)

func main() {
	x := fpmoney.FromInt(3, fpmoney.SGD)
	y := fpmoney.FromInt(9, fpmoney.SGD)
	fmt.Println(y == x.Mul(3))
}
Output:

true
Example (Equality_same_currency) ¶
package main

import (
	"fmt"

	_ "embed"
	"github.com/nikolaydubina/fpmoney"
)

func main() {
	x := fpmoney.FromInt(10, fpmoney.SGD)
	y := fpmoney.FromInt(10, fpmoney.SGD)
	fmt.Println(y == x)
}
Output:

true
Example (Equality_wrong_currency) ¶
package main

import (
	"fmt"

	_ "embed"
	"github.com/nikolaydubina/fpmoney"
)

func main() {
	x := fpmoney.FromInt(10, fpmoney.USD)
	y := fpmoney.FromInt(10, fpmoney.SGD)
	fmt.Println(y == x)
}
Output:

false

func FromFloat ¶

func FromFloat[T ~float32 | ~float64](v T, currency Currency) Amount
Example ¶
package main

import (
	"fmt"

	_ "embed"
	"github.com/nikolaydubina/fpmoney"
)

func main() {
	x := fpmoney.FromFloat(144.96, fpmoney.SGD)
	fmt.Println(x)
}
Output:

144.96 SGD

func FromInt ¶

func FromInt[T ~int | ~int8 | ~int16 | ~int32 | ~int64](v T, currency Currency) Amount

func FromIntScaled ¶

func FromIntScaled[T ~int | ~int8 | ~int16 | ~int32 | ~int64](v T, currency Currency) Amount

func (Amount) Add ¶

func (a Amount) Add(b Amount) Amount

func (Amount) Currency ¶

func (a Amount) Currency() Currency

func (Amount) Div ¶

func (a Amount) Div(b int) (part Amount, remainder Amount)
Example (Part) ¶
package main

import (
	"fmt"

	_ "embed"
	"github.com/nikolaydubina/fpmoney"
)

func main() {
	x := fpmoney.FromInt(1, fpmoney.SGD)
	a, r := x.Div(3)
	fmt.Println(a, r)
}
Output:

0.33 SGD 0.01 SGD
Example (Whole) ¶
package main

import (
	"fmt"

	_ "embed"
	"github.com/nikolaydubina/fpmoney"
)

func main() {
	x := fpmoney.FromInt(1, fpmoney.SGD)
	a, r := x.Div(5)
	fmt.Println(a, r)
}
Output:

0.2 SGD 0 SGD

func (Amount) Float32 ¶

func (a Amount) Float32() float32

func (Amount) Float64 ¶

func (a Amount) Float64() float64

func (Amount) GreaterThan ¶

func (a Amount) GreaterThan(b Amount) bool

func (Amount) GreaterThanOrEqual ¶

func (a Amount) GreaterThanOrEqual(b Amount) bool

func (Amount) LessThan ¶

func (a Amount) LessThan(b Amount) bool

func (Amount) LessThanOrEqual ¶

func (a Amount) LessThanOrEqual(b Amount) bool

func (Amount) MarshalJSON ¶

func (a Amount) MarshalJSON() ([]byte, error)

func (Amount) Mul ¶

func (a Amount) Mul(b int) Amount

func (Amount) String ¶

func (a Amount) String() string

func (Amount) Sub ¶

func (a Amount) Sub(b Amount) Amount

func (*Amount) UnmarshalJSON ¶

func (a *Amount) UnmarshalJSON(b []byte) (err error)

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

type Currency ¶ added in v0.4.0

type Currency struct {
	// contains filtered or unexported fields
}

Currency is ISO 4217 without deprecated currencies.

func (Currency) Exponent ¶ added in v0.4.0

func (c Currency) Exponent() uint8

Exponent returns the decimal point location.

func (Currency) MarshalText ¶ added in v0.4.0

func (s Currency) MarshalText() ([]byte, error)

func (Currency) String ¶ added in v0.4.0

func (c Currency) String() string

func (*Currency) UnmarshalText ¶ added in v0.4.0

func (s *Currency) UnmarshalText(text []byte) error

type ErrCurrencyMismatch ¶

type ErrCurrencyMismatch struct {
	A, B Currency
}

func NewErrCurrencyMismatch ¶

func NewErrCurrencyMismatch() *ErrCurrencyMismatch

func (*ErrCurrencyMismatch) Error ¶

func (e *ErrCurrencyMismatch) Error() string

type ErrWrongCurrencyString ¶ added in v1.0.1

type ErrWrongCurrencyString struct{}

func NewErrWrongCurrencyString ¶ added in v1.0.1

func NewErrWrongCurrencyString() *ErrWrongCurrencyString

func (*ErrWrongCurrencyString) Error ¶ added in v1.0.1

func (e *ErrWrongCurrencyString) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL