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

fulus

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

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

Go to latest
Published: Apr 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

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.

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

	// 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")
)
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]) 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]) 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]) 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]) 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]) 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

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