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/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)
ratio := fulus.Ratio[currency.EUR, currency.USD]{
Numerator: 104565, // 1.04565 represented as 104565/100000
Denominator: 100000,
}
eurInUsd, _, err := fulus.Convert(eur, ratio, fulus.RoundTruncate)
if err != nil {
panic(err)
}
fmt.Println(eurInUsd) // $5.22
usd, err = usd.Add(eurInUsd)
if err != nil {
panic(err)
}
fmt.Println(usd) // $15.22
fmt.Println(usd.Format(locale.FR)) // 15,22 $US
}This prevents common mistakes like:
- Accidentally mixing different currencies in calculations
- Using floating point numbers for money (uses int64 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.
Let's introduce kanna currency.
package main
import (
"fmt"
"github.com/khatibomar/fulus"
"github.com/khatibomar/fulus/currency"
"github.com/khatibomar/fulus/locale"
)
var _ currency.Currency = KANNA{}
type KANNA struct{}
func (k KANNA) Code() string { return "KANNA" }
func (k KANNA) Name() string { return "Kanna Kamui" }
func (k KANNA) Number() string { return "001" }
func (k KANNA) MinorUnits() int { return 2 }
func (k KANNA) FormatInfo(loc locale.Locale) currency.FormatInfo {
switch loc {
case locale.JA:
return currency.FormatInfo{
Symbol: "🐲",
Format: "#,##0.00 ¤",
GroupSeparator: "⚔︎",
DecimalSeparator: "🦖",
MinusSign: "⛔",
}
default:
return currency.FormatInfo{
Symbol: "🐉",
Format: "¤ #,##0.00",
GroupSeparator: ",",
DecimalSeparator: ".",
MinusSign: "-",
}
}
}
func main() {
kanna := fulus.NewMoney[KANNA](-1000000)
fmt.Println(kanna) // -🐉 10,000.00
kanna, _ = kanna.Mul(2)
fmt.Println(kanna) // -🐉 20,000.00
fmt.Println(kanna.Format(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
- JSON marshaling/unmarshaling support
- Explicit conversion rounding modes (truncate, half-up, half-even)
- Decimal string parsing with minor-unit validation
- database/sql integration via Scanner and Valuer
Fulus provides basic arithmetic methods with built-in overflow and error checking:
usd10 := fulus.NewMoney[currency.USD](1000)
usd20 := fulus.NewMoney[currency.USD](2000)
// Addition and Subtraction
usd30, err := usd10.Add(usd20)
usd10, err = usd30.Sub(usd20)
// Multiplication
usd20, err = usd10.Mul(2)
// Division with explicit rounding
usd5, err := usd10.Div(2, fulus.RoundHalfUp)
// Absolute value and Negation
absUsd, err := fulus.NewMoney[currency.USD](-1000).Abs() // $10.00
negUsd, err := usd10.Neg() // -$10.00For ergonomic method chaining when you are confident about bounds (panics on overflow), you can use the Must variants:
usd10 := fulus.NewMoney[currency.USD](1000)
usd20 := fulus.NewMoney[currency.USD](2000)
usd30 := fulus.NewMoney[currency.USD](3000)
// Method chaining
result := usd10.MustAdd(usd20).MustSub(usd30).MustMul(2) // $0.00Fulus provides standard comparison methods to safely compare money values of the same currency:
usd10 := fulus.NewMoney[currency.USD](1000)
usd20 := fulus.NewMoney[currency.USD](2000)
fmt.Println(usd10.GreaterThan(usd20)) // false
fmt.Println(usd10.LessThan(usd20)) // true
fmt.Println(usd10.Equal(usd20)) // false
fmt.Println(usd10.IsZero()) // false
fmt.Println(usd10.IsPositive()) // true
fmt.Println(usd10.Cmp(usd20)) // -1Use Convert with an explicit rounding mode:
eur := fulus.NewMoney[currency.EUR](5) // €0.05
ratio := fulus.Ratio[currency.EUR, currency.USD]{Numerator: 1, Denominator: 2}
usdTrunc, _, _ := fulus.Convert(eur, ratio, fulus.RoundTruncate) // $0.02
usdHalfUp, _, _ := fulus.Convert(eur, ratio, fulus.RoundHalfUp) // $0.03
usdHalfEven, _, _ := fulus.Convert(eur, ratio, fulus.RoundHalfEven)
fmt.Println(usdTrunc, usdHalfUp, usdHalfEven)Use ParseMoney to parse canonical decimal strings safely:
usd, err := fulus.ParseMoney[currency.USD]("123.45")
if err != nil {
panic(err)
}
fmt.Println(usd) // $123.45ParseMoney validates fractional scale against the currency minor units and rejects malformed formats.
Fulus provides helper functions to parse real-world float or string exchange rates (e.g., from an FX API) into the strict Ratio struct used for conversion:
// Parse a decimal string
ratioStr, err := fulus.ParseRatioString[currency.EUR, currency.USD]("1.07203")
if err != nil {
panic(err)
}
// Parse a float64
ratioFloat, err := fulus.ParseRatioFloat64[currency.EUR, currency.USD](1.07203)
if err != nil {
panic(err)
}Money[T] implements driver.Valuer and sql.Scanner.
var m fulus.Money[currency.USD]
if err := m.Scan(`{"amount":"1050","currency":"USD"}`); err != nil {
panic(err)
}
v, err := m.Value()
if err != nil {
panic(err)
}
fmt.Println(v)This library was inspired by the blog post How to deal with Money in Software by Christian Sejersen.
