Help us shape the future of Go! Take the 2026 Go Developer Survey

fulus

package module
v1.0.0-beta.4 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

Fulus

CI status GoDoc Go Report

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/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
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.

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

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
  • 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

Arithmetic Operations

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.00

For 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.00

Comparison Operations

Fulus 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))              // -1

Conversion Rounding Modes

Use 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)

Parse Decimal Strings

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.45

ParseMoney validates fractional scale against the currency minor units and rejects malformed formats.

Parse Exchange Rates

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

SQL Integration

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)

Credits

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

Documentation

Overview

Package fulus provides utilities for handling currency conversion and calculations.

Fulus helps developers work with different currencies and monetary operations in a type-safe and efficient manner. It includes features for currency parsing, formatting, conversion, and basic arithmetic operations while maintaining precision and correctness.

The package supports:

  • type-safe arithmetic across currency types (Add, Sub, Mul, Div, Abs, Neg)
  • configurable conversion rounding policies
  • decimal string parsing with minor-unit validation
  • JSON and database/sql interoperability

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrValidation is the error returned when money validation fails
	ErrValidation = errors.New("money validation error")

	// ErrOverflow indicates an arithmetic operation would overflow
	ErrOverflow = errors.New("arithmetic operation would overflow")

	// ErrInvalidChunks indicates an invalid number of chunks for distribution
	ErrInvalidChunks = errors.New("number of chunks must be positive")

	// ErrZeroDenominator indicates division by zero in conversion
	ErrZeroDenominator = errors.New("denominator cannot be zero")

	// ErrDivisionByZero indicates division by zero
	ErrDivisionByZero = errors.New("division by zero")

	// ErrNoRatios indicates no ratios were provided for allocation
	ErrNoRatios = errors.New("no ratios provided")

	// ErrNegativeOrZeroRatios indicates negative ratios in allocation
	ErrNegativeOrZeroRatios = errors.New("ratios must be positive")

	// ErrInvalidRoundingMode indicates unsupported rounding mode
	ErrInvalidRoundingMode = errors.New("invalid rounding mode")

	// ErrInvalidAmountFormat indicates amount string cannot be parsed
	ErrInvalidAmountFormat = errors.New("invalid amount format")

	// ErrScaleMismatch indicates decimal digits exceed currency minor units
	ErrScaleMismatch = errors.New("amount scale exceeds currency minor units")

	// ErrInvalidExchangeRate indicates an invalid exchange rate format or value
	ErrInvalidExchangeRate = errors.New("invalid exchange rate")
)
View Source
var (
	DefaultLocale = locale.EN
)

DefaultLocale is the fallback locale when none is specified

Functions

func Convert

func Convert[F, T currency.Currency](m Money[F], ratio Ratio[F, T], mode RoundingMode) (Money[T], ConversionResult[F, T], error)

Convert performs conversion with an explicit rounding strategy. The ratio should be provided as (numerator, denominator) representing numerator/denominator. Returns both the converted Money value and the actual ratio used after rounding.

Example
eur := NewMoney[currency.EUR](500) // €5.00
ratio := Ratio[currency.EUR, currency.USD]{
	Numerator:   104565, // 1.04565 represented as 104565/100000
	Denominator: 100000,
}
eurInUsd, _, err := Convert(eur, ratio, RoundTruncate)
if err != nil {
	panic(err)
}
fmt.Println(eurInUsd)
Output:
$5.22

Types

type Allocation

type Allocation[T currency.Currency] struct {
	Parts []Money[T]
	Total Money[T]
}

Allocation represents how money is divided according to ratios

type ConversionResult

type ConversionResult[F currency.Currency, T currency.Currency] struct {
	// Amount stores the resulting converted monetary value
	Amount int64
	// ActualRate stores the precise conversion rate that was actually applied
	ActualRate Ratio[F, T]
}

ConversionResult holds both the converted amount and the actual ratio used

type Distribution

type Distribution struct {
	// SmallerChunkSize represents the value of the smaller portions in the distribution
	SmallerChunkSize int64
	// SmallerCount represents how many smaller chunks are in the distribution
	SmallerCount int64
	// LargerChunkSize represents the value of the larger portions in the distribution
	LargerChunkSize int64
	// LargerCount represents how many larger chunks are in the distribution
	LargerCount int64
}

Distribution represents how to split money into chunks

type Money

type Money[T currency.Currency] struct {

	// Currency represents the type of currency for this money value
	currency.Currency
	// contains filtered or unexported fields
}

Money represents a monetary value in a specific currency.

func NewMoney

func NewMoney[T currency.Currency](amount int64) Money[T]

NewMoney creates a new Money instance with the given amount and currency. The amount should be specified in the currency's smallest sub-unit (e.g., cents for USD, pence for GBP). For example: USD 10.50 should be passed as 1050 EUR 5.99 should be passed as 599

func ParseMoney

func ParseMoney[T currency.Currency](amount string) (Money[T], error)

ParseMoney parses a canonical decimal amount into Money using the currency's minor units. Supported format: optional sign (+/-), digits, optional decimal point and digits.

func (Money[T]) Abs

func (m Money[T]) Abs() (Money[T], error)

Abs returns the absolute value of the money amount. Returns ErrOverflow if the amount is math.MinInt64.

func (Money[T]) Add

func (m Money[T]) Add(other Money[T]) (Money[T], error)

Add performs addition of two Money values of the same currency. Returns ErrOverflow if the operation would overflow int64.

func (Money[T]) Allocate

func (m Money[T]) Allocate(ratios []int64) (Allocation[T], error)

Allocate divides money according to provided ratios

Example
dollars := NewMoney[currency.USD](10000) // 100.00 USD

// Allocate in ratio of [1,1,2]
allocation, err := dollars.Allocate([]int64{1, 1, 2})
if err != nil {
	fmt.Println("Error:", err)
	return
}

for i, part := range allocation.Parts {
	fmt.Printf("Part %d: %v\n", i+1, part)
}
Output:
Part 1: $25.00
Part 2: $25.00
Part 3: $50.00

func (Money[T]) Amount

func (m Money[T]) Amount() int64

Amount returns the internal amount value in the currency's smallest unit. For example, returns cents for USD or pence for GBP.

func (Money[T]) Cmp

func (m Money[T]) Cmp(other Money[T]) int

Cmp compares two Money values and returns: -1 if m < other

0 if m == other

+1 if m > other

func (Money[T]) Distribute

func (m Money[T]) Distribute(chunks int64) (Distribution, error)

Distribute splits the money amount into the specified number of chunks Returns a Distribution describing how to split the money

Example
dollars := NewMoney[currency.USD](10000) // 100.00 USD

// Distribute into 3 chunks
dist, err := dollars.Distribute(3)
if err != nil {
	fmt.Println("Error:", err)
	return
}

fmt.Printf("Smaller chunks: %d x $%.2f\n",
	dist.SmallerCount,
	float64(dist.SmallerChunkSize)/100)
fmt.Printf("Larger chunks: %d x $%.2f\n",
	dist.LargerCount,
	float64(dist.LargerChunkSize)/100)

// Verify total
total := (dist.SmallerChunkSize * dist.SmallerCount) +
	(dist.LargerChunkSize * dist.LargerCount)
fmt.Printf("Total: $%.2f\n", float64(total)/100)
Output:
Smaller chunks: 2 x $33.33
Larger chunks: 1 x $33.34
Total: $100.00

func (Money[T]) Div

func (m Money[T]) Div(divisor int64, mode RoundingMode) (Money[T], error)

Div divides the Money value by a scalar value using the specified rounding mode. Returns ErrDivisionByZero if divisor is 0. Returns ErrOverflow if the operation would overflow int64. Returns ErrInvalidRoundingMode if the rounding mode is unsupported.

func (Money[T]) Equal

func (m Money[T]) Equal(other Money[T]) bool

Equal returns true if the two Money values are equal.

func (Money[T]) Format

func (m Money[T]) Format(locale locale.Locale) string

Format returns a formatted string representation of the Money value for the specified locale.

func (Money[T]) GreaterThan

func (m Money[T]) GreaterThan(other Money[T]) bool

GreaterThan returns true if the Money value is greater than the other.

func (Money[T]) GreaterThanOrEqual

func (m Money[T]) GreaterThanOrEqual(other Money[T]) bool

GreaterThanOrEqual returns true if the Money value is greater than or equal to the other.

func (Money[T]) IsNegative

func (m Money[T]) IsNegative() bool

IsNegative returns true if the Money amount is less than zero.

func (Money[T]) IsPositive

func (m Money[T]) IsPositive() bool

IsPositive returns true if the Money amount is greater than zero.

func (Money[T]) IsZero

func (m Money[T]) IsZero() bool

IsZero returns true if the Money amount is zero.

func (Money[T]) LessThan

func (m Money[T]) LessThan(other Money[T]) bool

LessThan returns true if the Money value is less than the other.

func (Money[T]) LessThanOrEqual

func (m Money[T]) LessThanOrEqual(other Money[T]) bool

LessThanOrEqual returns true if the Money value is less than or equal to the other.

func (Money[T]) MarshalJSON

func (m Money[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface Serializes the amount as a string to preserve precision

func (Money[T]) Mul

func (m Money[T]) Mul(scale int64) (Money[T], error)

Mul multiplies the Money value by a scalar value. Returns ErrOverflow if the operation would overflow int64. If scale is 0, sets the amount to 0 and returns nil.

func (Money[T]) MustAdd

func (m Money[T]) MustAdd(other Money[T]) Money[T]

MustAdd performs addition of two Money values of the same currency. Panics if the operation would overflow int64.

func (Money[T]) MustMul

func (m Money[T]) MustMul(scale int64) Money[T]

MustMul multiplies the Money value by a scalar value. Panics if the operation would overflow int64.

func (Money[T]) MustSub

func (m Money[T]) MustSub(other Money[T]) Money[T]

MustSub performs subtraction of two Money values of the same currency. Panics if the operation would overflow int64.

func (Money[T]) Neg

func (m Money[T]) Neg() (Money[T], error)

Neg returns the negated value of the money amount. Returns ErrOverflow if the amount is math.MinInt64.

func (*Money[T]) Scan

func (m *Money[T]) Scan(value any) error

Scan implements sql.Scanner for database/sql.

func (Money[T]) String

func (m Money[T]) String() string

String returns a formatted string representation of the Money value using the default locale. This implements the fmt.Stringer interface.

func (Money[T]) Sub

func (m Money[T]) Sub(other Money[T]) (Money[T], error)

Sub performs subtraction of two Money values of the same currency. Returns ErrOverflow if the operation would overflow int64.

func (*Money[T]) UnmarshalJSON

func (m *Money[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON implements the json.Unmarshaler interface Expects amount as a string to maintain precision

func (Money[T]) Validate

func (m Money[T]) Validate(min, max int64) error

Validate checks if the money amount falls within the specified range [min, max]. Returns an error if the amount is outside the range.

func (Money[T]) Value

func (m Money[T]) Value() (driver.Value, error)

Value implements driver.Valuer for database/sql.

type Ratio

type Ratio[F currency.Currency, T currency.Currency] struct {
	// Numerator is the top number in the fraction (e.g., 107203 for 1.07203)
	Numerator int64
	// Denominator is the bottom number in the fraction (e.g., 100000 for precise decimal representation)
	Denominator int64
}

Ratio represents a fraction used for conversion rates

func ParseRatioFloat64

func ParseRatioFloat64[F currency.Currency, T currency.Currency](rate float64) (Ratio[F, T], error)

ParseRatioFloat64 parses a float64 representation of an exchange rate into a Ratio. It formats the float to a string to avoid floating point precision issues.

func ParseRatioString

func ParseRatioString[F currency.Currency, T currency.Currency](rate string) (Ratio[F, T], error)

ParseRatioString parses a string representation of an exchange rate into a Ratio

type RoundingMode

type RoundingMode int

RoundingMode controls how division results are rounded in conversion.

const (
	// RoundTruncate rounds toward zero (default behavior).
	RoundTruncate RoundingMode = iota
	// RoundHalfUp rounds to nearest, ties away from zero.
	RoundHalfUp
	// RoundHalfEven rounds to nearest, ties to even.
	RoundHalfEven
)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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