Art generated by copilot
A Go library for handling monetary values and currency operations with type safety. It support formatting based on Locale provided by https://cldr.unicode.org/
Money calculations require strict type safety to prevent costly mistakes. Fulus enforces this at compile time using Go generics:
package main
import (
"fmt"
"github.com/khatibomar/fulus"
"github.com/khatibomar/fulus/currency"
"github.com/khatibomar/fulus/format"
"github.com/khatibomar/fulus/locale"
)
func main() {
usd := fulus.NewMoney[currency.USD](1000) // $10.00
eur := fulus.NewMoney[currency.EUR](500) // €5.00
// This will not compile. compiler will throw this error.
// cannot use eur (variable of struct type fulus.Money[currency.EUR])
// as fulus.Money[currency.USD] value in argument to usd.Add
// usd.Add(eur)
rate, err := fulus.ParseRate[currency.EUR, currency.USD]("1.04565")
if err != nil {
panic(err)
}
eurInUsd, err := fulus.Convert(eur, rate, fulus.RoundTruncate)
if err != nil {
panic(err)
}
fmt.Println(eurInUsd) // USD 5.22
usd, err = usd.Add(eurInUsd)
if err != nil {
panic(err)
}
fmt.Println(usd) // USD 15.22
fmt.Println(format.Money(usd, locale.FR)) // 15,22 $US
}This prevents common mistakes like:
- Accidentally mixing different currencies in calculations
- Using floating point numbers for money (uses a 128-bit integer internally)
- Imprecise currency conversions
Don't see your currency in the list? No problem! You can easily create custom currency types that are specific to your financial domain's needs.
A currency type must be an empty struct type. Money[T] uses the constraint currency.Unit,
so a type with fields or an interface type such as currency.Currency does not compile.
Let's introduce kanna currency.
package main
import (
"fmt"
"github.com/khatibomar/fulus"
"github.com/khatibomar/fulus/format"
"github.com/khatibomar/fulus/locale"
)
// A currency needs only Code and MinorUnits.
type KANNA struct{}
func (KANNA) Code() string { return "KANNA" }
func (KANNA) MinorUnits() int { return 2 }
// FormatInfo is optional. It implements format.Formatter. Without it, format uses the CLDR data of the code.
func (KANNA) FormatInfo(loc locale.Locale) format.Info {
if loc == locale.JA {
return format.Info{Symbol: "🐲", Pattern: "#,##0.00 ¤", GroupSeparator: "⚔︎", DecimalSeparator: "🦖", MinusSign: "⛔"}
}
return format.Info{Symbol: "🐉", Pattern: "¤ #,##0.00", GroupSeparator: ",", DecimalSeparator: ".", MinusSign: "-"}
}
func main() {
kanna := fulus.NewMoney[KANNA](-1000000)
fmt.Println(format.Money(kanna, locale.EN)) // -🐉 10,000.00
kanna, _ = kanna.Mul(2)
fmt.Println(kanna) // KANNA -20000.00
fmt.Println(format.Money(kanna, locale.JA)) // ⛔20⚔︎000🦖00 🐲
}- Type-safe money operations using Go generics
- Prevention of invalid currency operations at compile time
- Safe decimal arithmetic using integer math
- Support for distribution and allocation of money (largest remainder method)
- Runtime currencies with
AnyMoneyand a currency registry - Seven explicit rounding modes plus
RoundUnnecessary, aFactortype for rates, percentages and basis points, and CLDR cash rounding - Exact exchange rates with
Rate[Base, Quote], cross rates that the compiler checks, andAnyRatefor runtime currencies - CLDR formatting and parsing of localized strings in the
formatpackage, with locale matching from BCP 47 and POSIX tags - JSON with decimal strings, text,
log/slog, and database/sql forNUMERICandBIGINTcolumns, withNullMoneyfor NULL values google.type.Moneyconversion in the separatefulusprotomodule
The amount is a signed 128-bit integer in minor units.
The largest value is about 1.7 × 10^36 for a currency with 2 minor units.
Every operation returns ErrOverflow instead of a wrong result when the result does not fit.
A currency with many minor units has a smaller range.
For example, a token with 18 minor units can hold about 1.7 × 10^20 units.
A Rate or a Factor is a fraction of two int64 terms, so it holds at most about 18 significant digits.
ParseRate and ParseFactor return ErrOverflow for a value with more digits.
- Usage: arithmetic, comparison, rounding, runtime currencies, locales, exchange rates, SQL and protocol buffers.
- Rounding and overflow: which operations round, how they round, and the range of each currency.
- Design decisions: why Fulus uses 128-bit minor units and generics, and what that costs.
- Release notes, CONTRIBUTING and SECURITY.
| Fulus | Rhymond/go-money | bojanz/currency | |
|---|---|---|---|
| Amount | 128-bit minor units | int64 minor units |
Arbitrary-precision decimal |
| Currency check | Compile time, with a type parameter | Run time | Run time |
| Digits after the minor units | No | No | Yes |
| Overflow | ErrOverflow |
Not checked | No overflow |
| Rounding | 7 modes and RoundUnnecessary, explicit in each call |
To a whole major unit only | 5 modes |
| Leftover units of an allocation | Largest remainder | One each to the first parts | One each to the first parts |
| Locale formatting | All CLDR locales, checked against ICU | One format for each currency | CLDR modern locales |
| Parse formatted amounts | Yes | No | Yes |
| Dependencies | None | None | cockroachdb/apd |
Use Fulus when the currency is known at compile time and 128-bit minor units are enough.
Use bojanz/currency when you need more digits than the minor units, or an unlimited range.
Arithmetic uses 128-bit and 192-bit integer math from math/bits and does not allocate.
math/big is used only to parse rates and factors, and when an intermediate result does not fit in 128 bits.
Locale data is in tables indexed by locale, so a format lookup does not depend on the number of locales.
Results of go test -bench . -benchmem on an AMD Ryzen AI 9 HX PRO 370:
| Operation | Time | Allocations |
|---|---|---|
Add, Sub |
2 ns | 0 |
Mul |
4 ns | 0 |
Div, MulFactor, Convert |
9–10 ns | 0 |
Allocate (3 parts) |
34 ns | 1 |
format.Money |
130 ns | 1 |
ParseMoney |
21 ns | 0 |
MarshalJSON (with json.Marshal) |
170 ns | 4 |
Fulus uses semantic versioning.
- Before v1.0.0, a release can have breaking changes. The release notes list them.
- From v1.0.0, a minor or patch release does not remove or change an exported name.
- To remove a name, we first mark it with
Deprecated:in its doc comment and in the release notes. We remove it only in the next major version. - An update of the CLDR data is not a breaking change. It can change the output of
format.Moneyin a minor release. - A change of the minor units of a currency is a breaking change. CI checks the minor units against the latest release.
Do not store the output of
format.Money. Store the amount withValue,MarshalJSONorDecimal. - CI compares the API of each pull request with the latest release.
Fulus uses the Apache License 2.0. The generated locale and currency data comes from CLDR, which uses the Unicode License v3.
This library was inspired by the blog post How to deal with Money in Software by Christian Sejersen.