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

fulus

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

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

Go to latest
Published: Feb 15, 2025 License: Apache-2.0 Imports: 8 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{
		Numerator:   104565, // 1.04565 represented as 104565/100000
		Denominator: 100000,
	}
	eurInUsd, _, err := fulus.Convert[currency.USD](eur, ratio)
	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.CurrencyFormatInfo {
	switch loc {
	case locale.JA:
		return currency.CurrencyFormatInfo{
			Symbol:           "🐲",
			Format:           "#,##0.00 ¤",
			GroupSeparator:   "⚔︎",
			DecimalSeparator: "🦖",
			MinusSign:        "⛔",
		}
	default:
		return currency.CurrencyFormatInfo{
			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

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.

Index

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")
)
View Source
var (
	DefaultLocale = locale.EN
)

DefaultLocale is the fallback locale when none is specified

Functions

func Convert

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

Convert performs a conversion using a ratio while maintaining accuracy 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:

// Convert 100 EUR to CHF at rate 1.072032 CHF/EUR
eur := NewMoney[currency.EUR](10000) // 100.00 EUR
ratio := Ratio{
	Numerator:   107203,  // 1.07203 represented as 107203/100000
	Denominator: 100000,
}
chf, result, _ := fulus.Convert[currency.CHF](eur, ratio)
// chf will be 107.20 CHF (10720 cents)
// result.ActualRate shows the actual conversion rate used after rounding

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 struct {
	// Amount stores the resulting converted monetary value
	Amount int64
	// ActualRate stores the precise conversion rate that was actually applied
	ActualRate Ratio
}

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 (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: $100 allocated by ratios [1,1,2] would give [$25,$25,$50]

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:

money := NewMoney[currency.USD](1000) // $10.00
dist, _ := money.Distribute(3)
// dist will contain:
// SmallerChunkSize: 333 ($3.33) x 2 chunks
// LargerChunkSize:  334 ($3.34) x 1 chunk
// Total: (333 * 2) + (334 * 1) = 1000

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]) 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.

type Ratio

type Ratio 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

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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