Skip to content
khatibomarPublic

About

type-safe Go money library

Resources

Contributing

Security policy

Stars

13 stars

Watchers

1 watching

Forks

Latest commit

 

History

95 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fulus

CI status Go Reference OpenSSF Scorecard

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/

Why?

type safety

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

defining own currency types

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 🐲
}

Features

  • 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 AnyMoney and a currency registry
  • Seven explicit rounding modes plus RoundUnnecessary, a Factor type for rates, percentages and basis points, and CLDR cash rounding
  • Exact exchange rates with Rate[Base, Quote], cross rates that the compiler checks, and AnyRate for runtime currencies
  • CLDR formatting and parsing of localized strings in the format package, with locale matching from BCP 47 and POSIX tags
  • JSON with decimal strings, text, log/slog, and database/sql for NUMERIC and BIGINT columns, with NullMoney for NULL values
  • google.type.Money conversion in the separate fulusproto module

Limits

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.

Documentation

Comparison

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.

Performance

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

Stability

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.Money in 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 with Value, MarshalJSON or Decimal.
  • CI compares the API of each pull request with the latest release.

License

Fulus uses the Apache License 2.0. The generated locale and currency data comes from CLDR, which uses the Unicode License v3.

Credits

This library was inspired by the blog post How to deal with Money in Software by Christian Sejersen.

About

type-safe Go money library

Resources

Contributing

Security policy

Stars

13 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages