currency

package module
v1.2.3 Latest Latest
Warning

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

Go to latest
Published: Apr 19, 2024 License: MIT Imports: 11 Imported by: 18

README

currency Build Coverage Status Go Report Card PkgGoDev

Handles currency amounts, provides currency information and formatting.

Powered by CLDR v45, in just ~30kb of data.

Backstory: https://bojanz.github.io/price-currency-handling-go/

Features

  1. All currency codes, their numeric codes and fraction digits.
  2. Currency symbols and formats for all locales.
  3. Country mapping (country code => currency code).
  4. Amount struct, with value semantics (Fowler's Money pattern)
  5. Formatter, for formatting amounts and parsing formatted amounts.
    amount, _ := currency.NewAmount("275.98", "EUR")
    total, _ := amount.Mul("4")

    locale := currency.NewLocale("fr")
    formatter := currency.NewFormatter(locale)
    fmt.Println(formatter.Format(total)) // 1 103,92 €

    // Convert the amount to Iranian rial and show it in Farsi.
    total, _ = total.Convert("IRR", "45.538")
    total = total.Round()
    locale = currency.NewLocale("fa")
    formatter = currency.NewFormatter(locale)
    fmt.Println(formatter.Format(total)) // ‎ریال ۵۰٬۲۷۰

Design goals

Real decimal implementation under the hood.

Currency amounts can't be floats. Storing integer minor units (2.99 => 299) becomes problematic once there are multiple currencies (difficult to sort in the DB), or there is a need for sub-minor-unit precision (due to merchant or tax requirements, etc). A real arbitrary-precision decimal type is required. Since Go doesn't have one natively, a userspace implementation is used, provided by the cockroachdb/apd package. The Amount struct provides an easy to use abstraction on top of it, allowing the underlying implementation to be replaced in the future without a backwards compatibility break.

Smart filtering of CLDR data.

The "modern" subset of CLDR locales is used, reducing the list from ~560 to ~370 locales.

Once gathered, locales are filtered to remove all data not used by this package, and then deduplicated by parent (e.g. don't keep fr-CH if fr has the same data).

Currency symbols are grouped together to avoid repetition. For example:

"ARS": {
    {"ARS", []string{"en", "fr-CA"}},
    {"$", []string{"es-AR"}},
    {"$AR", []string{"fr"}},
}

Currency names are not included because they are rarely shown, but need significant space. Instead, they can be fetched on the frontend via Intl.DisplayNames.

Easy to compare.

Amount structs can be compared via google/go-cmp thanks to the built-in Equal() method.

Usable with a PostgreSQL composite type.

Thanks to the driver.Valuer and sql.Scanner interfaces, applications using the pgx driver can store amounts in a composite type.

Example schema:

CREATE TYPE price AS (
   number NUMERIC,
   currency_code TEXT
);

CREATE TABLE products (
   id CHAR(26) PRIMARY KEY,
   name TEXT NOT NULL,
   price price NOT NULL,
   created_at TIMESTAMPTZ NOT NULL,
   updated_at TIMESTAMPTZ
);

Note that the number and currency_code columns can have any name, only their ordering matters.

Example struct:

type Product struct {
	ID          string
	Name        string
	Price       currency.Amount
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

Example scan:

p := Product{}
row := tx.QueryRow(ctx, `SELECT id, name, price, created_at, updated_at FROM products WHERE id = $1`, id)
err := row.Scan(&p.ID, &p.Name, &p.Price, &p.CreatedAt, &p.UpdatedAt)

See our database integration notes for other examples (MySQL/MariaDB, SQLite).

Documentation

Overview

Package currency handles currency amounts, provides currency information and formatting.

Index

Examples

Constants

View Source
const CLDRVersion = "45.0.0"

CLDRVersion is the CLDR version from which the data is derived.

View Source
const DefaultDigits uint8 = 255

DefaultDigits is a placeholder for each currency's number of fraction digits.

Variables

This section is empty.

Functions

func ForCountryCode added in v1.2.0

func ForCountryCode(countryCode string) (currencyCode string, ok bool)

ForCountryCode returns the currency code for a country code.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	currencyCode, ok := currency.ForCountryCode("US")
	fmt.Println(currencyCode, ok)

	currencyCode, ok = currency.ForCountryCode("FR")
	fmt.Println(currencyCode, ok)

	// Non-existent country code.
	_, ok = currency.ForCountryCode("XX")
	fmt.Println(ok)
}
Output:

USD true
EUR true
false

func GetCurrencyCodes

func GetCurrencyCodes() []string

GetCurrencyCodes returns all known currency codes.

func GetDigits

func GetDigits(currencyCode string) (digits uint8, ok bool)

GetDigits returns the number of fraction digits for a currency code.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	digits, ok := currency.GetDigits("USD")
	fmt.Println(digits, ok)

	// Non-existent currency code.
	digits, ok = currency.GetDigits("XXX")
	fmt.Println(digits, ok)
}
Output:

2 true
0 false

func GetNumericCode

func GetNumericCode(currencyCode string) (numericCode string, ok bool)

GetNumericCode returns the numeric code for a currency code.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	numericCode, ok := currency.GetNumericCode("USD")
	fmt.Println(numericCode, ok)

	// Non-existent currency code.
	numericCode, ok = currency.GetNumericCode("XXX")
	fmt.Println(numericCode, ok)
}
Output:

840 true
000 false

func GetSymbol

func GetSymbol(currencyCode string, locale Locale) (symbol string, ok bool)

GetSymbol returns the symbol for a currency code.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	locale := currency.NewLocale("en")
	symbol, ok := currency.GetSymbol("USD", locale)
	fmt.Println(symbol, ok)

	// Non-existent currency code.
	symbol, ok = currency.GetSymbol("XXX", locale)
	fmt.Println(symbol, ok)
}
Output:

$ true
XXX false

func IsValid

func IsValid(currencyCode string) bool

IsValid checks whether a currency code is valid.

An empty currency code is considered valid.

Types

type Amount

type Amount struct {
	// contains filtered or unexported fields
}

Amount stores a decimal number with its currency code.

func NewAmount

func NewAmount(n, currencyCode string) (Amount, error)

NewAmount creates a new Amount from a numeric string and a currency code.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	amount, _ := currency.NewAmount("24.49", "USD")
	fmt.Println(amount)
	fmt.Println(amount.Number())
	fmt.Println(amount.CurrencyCode())
}
Output:

24.49 USD
24.49
USD

func NewAmountFromBigInt

func NewAmountFromBigInt(n *big.Int, currencyCode string) (Amount, error)

NewAmountFromBigInt creates a new Amount from a big.Int and a currency code.

func NewAmountFromInt64

func NewAmountFromInt64(n int64, currencyCode string) (Amount, error)

NewAmountFromInt64 creates a new Amount from an int64 and a currency code.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	firstAmount, _ := currency.NewAmountFromInt64(2449, "USD")
	secondAmount, _ := currency.NewAmountFromInt64(5000, "USD")
	thirdAmount, _ := currency.NewAmountFromInt64(60, "JPY")
	fmt.Println(firstAmount)
	fmt.Println(secondAmount)
	fmt.Println(thirdAmount)
}
Output:

24.49 USD
50.00 USD
60 JPY

func (Amount) Add

func (a Amount) Add(b Amount) (Amount, error)

Add adds a and b together and returns the result.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	firstAmount, _ := currency.NewAmount("20.99", "USD")
	secondAmount, _ := currency.NewAmount("3.50", "USD")
	totalAmount, _ := firstAmount.Add(secondAmount)
	fmt.Println(totalAmount)
}
Output:

24.49 USD
Example (Sum)
package main

import (
	"fmt"
	"strconv"

	"github.com/bojanz/currency"
)

func main() {
	// Any currency.Amount can be added to the zero value.
	var sum currency.Amount
	for i := 0; i <= 4; i++ {
		a, _ := currency.NewAmount(strconv.Itoa(i), "AUD")
		sum, _ = sum.Add(a)
	}

	fmt.Println(sum) // 0 + 1 + 2 + 3 + 4 = 10
}
Output:

10 AUD

func (Amount) BigInt

func (a Amount) BigInt() *big.Int

BigInt returns a in minor units, as a big.Int.

func (Amount) Cmp

func (a Amount) Cmp(b Amount) (int, error)

Cmp compares a and b and returns:

-1 if a <  b
0 if a == b
+1 if a >  b

func (Amount) Convert

func (a Amount) Convert(currencyCode, rate string) (Amount, error)

Convert converts a to a different currency.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	amount, _ := currency.NewAmount("20.99", "USD")
	amount, _ = amount.Convert("EUR", "0.91")
	fmt.Println(amount)
	fmt.Println(amount.Round())
}
Output:

19.1009 EUR
19.10 EUR

func (Amount) CurrencyCode

func (a Amount) CurrencyCode() string

CurrencyCode returns the currency code.

func (Amount) Div

func (a Amount) Div(n string) (Amount, error)

Div divides a by n and returns the result.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	totalAmount, _ := currency.NewAmount("99.99", "USD")
	amount, _ := totalAmount.Div("3")
	fmt.Println(amount)
}
Output:

33.33 USD

func (Amount) Equal

func (a Amount) Equal(b Amount) bool

Equal returns whether a and b are equal.

func (Amount) Int64

func (a Amount) Int64() (int64, error)

Int64 returns a in minor units, as an int64. If a cannot be represented in an int64, an error is returned.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	firstAmount, _ := currency.NewAmount("24.49", "USD")
	secondAmount, _ := currency.NewAmount("50", "USD")
	thirdAmount, _ := currency.NewAmount("60", "JPY")
	firstInt, _ := firstAmount.Int64()
	secondInt, _ := secondAmount.Int64()
	thirdInt, _ := thirdAmount.Int64()
	fmt.Println(firstInt, secondInt, thirdInt)
}
Output:

2449 5000 60

func (Amount) IsNegative

func (a Amount) IsNegative() bool

IsNegative returns whether a is negative.

func (Amount) IsPositive

func (a Amount) IsPositive() bool

IsPositive returns whether a is positive.

func (Amount) IsZero

func (a Amount) IsZero() bool

IsZero returns whether a is zero.

func (Amount) MarshalBinary

func (a Amount) MarshalBinary() ([]byte, error)

MarshalBinary implements the encoding.BinaryMarshaler interface.

func (Amount) MarshalJSON

func (a Amount) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface.

func (Amount) Mul

func (a Amount) Mul(n string) (Amount, error)

Mul multiplies a by n and returns the result.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	amount, _ := currency.NewAmount("20.99", "USD")
	taxAmount, _ := amount.Mul("0.20")
	fmt.Println(taxAmount)
	fmt.Println(taxAmount.Round())
}
Output:

4.1980 USD
4.20 USD

func (Amount) Number

func (a Amount) Number() string

Number returns the number as a numeric string.

func (Amount) Round

func (a Amount) Round() Amount

Round is a shortcut for RoundTo(currency.DefaultDigits, currency.RoundHalfUp).

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	firstAmount, _ := currency.NewAmount("12.345", "USD")
	secondAmount, _ := currency.NewAmount("12.345", "JPY")
	fmt.Println(firstAmount.Round())
	fmt.Println(secondAmount.Round())
}
Output:

12.35 USD
12 JPY

func (Amount) RoundTo

func (a Amount) RoundTo(digits uint8, mode RoundingMode) Amount

RoundTo rounds a to the given number of fraction digits.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	amount, _ := currency.NewAmount("12.345", "USD")
	for _, digits := range []uint8{4, 3, 2, 1, 0} {
		fmt.Println(amount.RoundTo(digits, currency.RoundHalfUp))
	}
}
Output:

12.3450 USD
12.345 USD
12.35 USD
12.3 USD
12 USD

func (*Amount) Scan

func (a *Amount) Scan(src interface{}) error

Scan implements the database/sql.Scanner interface.

Allows scanning amounts from a PostgreSQL composite type.

func (Amount) String

func (a Amount) String() string

String returns the string representation of a.

func (Amount) Sub

func (a Amount) Sub(b Amount) (Amount, error)

Sub subtracts b from a and returns the result.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	baseAmount, _ := currency.NewAmount("20.99", "USD")
	discountAmount, _ := currency.NewAmount("5.00", "USD")
	amount, _ := baseAmount.Sub(discountAmount)
	fmt.Println(amount)
}
Output:

15.99 USD
Example (Diff)
package main

import (
	"fmt"
	"strconv"

	"github.com/bojanz/currency"
)

func main() {
	// Any currency.Amount can be subtracted from the zero value.
	var diff currency.Amount
	for i := 0; i <= 4; i++ {
		a, _ := currency.NewAmount(strconv.Itoa(i), "AUD")
		diff, _ = diff.Sub(a)
	}

	fmt.Println(diff) // 0 - 1 - 2 - 3 - 4 = -10
}
Output:

-10 AUD

func (*Amount) UnmarshalBinary

func (a *Amount) UnmarshalBinary(data []byte) error

UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.

func (*Amount) UnmarshalJSON

func (a *Amount) UnmarshalJSON(data []byte) error

UnmarshalJSON implements the json.Unmarshaler interface.

func (Amount) Value

func (a Amount) Value() (driver.Value, error)

Value implements the database/driver.Valuer interface.

Allows storing amounts in a PostgreSQL composite type.

type Display

type Display uint8

Display represents the currency display type.

const (
	// DisplaySymbol shows the currency symbol.
	DisplaySymbol Display = iota
	// DisplayCode shows the currency code.
	DisplayCode
	// DisplayNone shows nothing, hiding the currency.
	DisplayNone
)

type Formatter

type Formatter struct {

	// AccountingStyle formats the amount using the accounting style.
	// For example, "-3.00 USD" in the "en" locale is formatted as "($3.00)" instead of "-$3.00".
	// Defaults to false.
	AccountingStyle bool
	// AddPlusSign inserts the plus sign in front of positive amounts.
	// Defaults to false.
	AddPlusSign bool
	// NoGrouping turns off grouping of major digits.
	// Defaults to false.
	NoGrouping bool
	// MinDigits specifies the minimum number of fraction digits.
	// All zeroes past the minimum will be removed (0 => no trailing zeroes).
	// Defaults to currency.DefaultDigits (e.g. 2 for USD, 0 for RSD).
	MinDigits uint8
	// MaxDigits specifies the maximum number of fraction digits.
	// Formatted amounts will be rounded to this number of digits.
	// Defaults to 6, so that most amounts are shown as-is (without rounding).
	MaxDigits uint8
	// RoundingMode specifies how the formatted amount will be rounded.
	// Defaults to currency.RoundHalfUp.
	RoundingMode RoundingMode
	// CurrencyDisplay specifies how the currency will be displayed (symbol/code/none).
	// Defaults to currency.DisplaySymbol.
	CurrencyDisplay Display
	// SymbolMap specifies custom symbols for individual currency codes.
	// For example, "USD": "$" means that the $ symbol will be used even if
	// the current locale's symbol is different ("US$", "$US", etc).
	SymbolMap map[string]string
	// contains filtered or unexported fields
}

Formatter formats and parses currency amounts.

func NewFormatter

func NewFormatter(locale Locale) *Formatter

NewFormatter creates a new formatter for the given locale.

func (*Formatter) Format

func (f *Formatter) Format(amount Amount) string

Format formats a currency amount.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	locale := currency.NewLocale("tr")
	formatter := currency.NewFormatter(locale)
	amount, _ := currency.NewAmount("1245.988", "EUR")
	fmt.Println(formatter.Format(amount))

	formatter.MaxDigits = 2
	fmt.Println(formatter.Format(amount))

	formatter.NoGrouping = true
	amount, _ = currency.NewAmount("1245", "EUR")
	fmt.Println(formatter.Format(amount))

	formatter.MinDigits = 0
	fmt.Println(formatter.Format(amount))

	formatter.CurrencyDisplay = currency.DisplayNone
	fmt.Println(formatter.Format(amount))
}
Output:

€1.245,988
€1.245,99
€1245,00
€1245
1245

func (*Formatter) Locale

func (f *Formatter) Locale() Locale

Locale returns the locale.

func (*Formatter) Parse

func (f *Formatter) Parse(s, currencyCode string) (Amount, error)

Parse parses a formatted amount.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	locale := currency.NewLocale("tr")
	formatter := currency.NewFormatter(locale)

	amount, _ := formatter.Parse("€1.234,59", "EUR")
	fmt.Println(amount)

	amount, _ = formatter.Parse("EUR 1.234,59", "EUR")
	fmt.Println(amount)

	amount, _ = formatter.Parse("1.234,59", "EUR")
	fmt.Println(amount)
}
Output:

1234.59 EUR
1234.59 EUR
1234.59 EUR

type InvalidCurrencyCodeError

type InvalidCurrencyCodeError struct {
	CurrencyCode string
}

InvalidCurrencyCodeError is returned when a currency code is invalid or unrecognized.

func (InvalidCurrencyCodeError) Error

func (e InvalidCurrencyCodeError) Error() string

type InvalidNumberError

type InvalidNumberError struct {
	Number string
}

InvalidNumberError is returned when a numeric string can't be converted to a decimal.

func (InvalidNumberError) Error

func (e InvalidNumberError) Error() string

type Locale

type Locale struct {
	Language  string
	Script    string
	Territory string
}

Locale represents a Unicode locale identifier.

func NewLocale

func NewLocale(id string) Locale

NewLocale creates a new Locale from its string representation.

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	firstLocale := currency.NewLocale("en-US")
	fmt.Println(firstLocale)
	fmt.Println(firstLocale.Language, firstLocale.Territory)

	// Locale IDs are normalized.
	secondLocale := currency.NewLocale("sr_rs_latn")
	fmt.Println(secondLocale)
	fmt.Println(secondLocale.Language, secondLocale.Script, secondLocale.Territory)
}
Output:

en-US
en US
sr-Latn-RS
sr Latn RS

func (Locale) GetParent

func (l Locale) GetParent() Locale

GetParent returns the parent locale for l.

Order:
1. Language - Script - Territory (e.g. "sr-Cyrl-RS")
2. Language - Script (e.g. "sr-Cyrl")
3. Language (e.g. "sr")
4. English ("en")
5. Empty locale ("")

Note that according to CLDR rules, certain locales have special parents. For example, the parent for "es-AR" is "es-419", and for "sr-Latn" it is "en".

Example
package main

import (
	"fmt"

	"github.com/bojanz/currency"
)

func main() {
	locale := currency.NewLocale("sr-Cyrl-RS")
	for {
		fmt.Println(locale)
		locale = locale.GetParent()
		if locale.IsEmpty() {
			break
		}
	}
}
Output:

sr-Cyrl-RS
sr-Cyrl
sr
en

func (Locale) IsEmpty

func (l Locale) IsEmpty() bool

IsEmpty returns whether l is empty.

func (Locale) MarshalText

func (l Locale) MarshalText() ([]byte, error)

MarshalText implements the encoding.TextMarshaler interface.

func (Locale) String

func (l Locale) String() string

String returns the string representation of l.

func (*Locale) UnmarshalText

func (l *Locale) UnmarshalText(b []byte) error

UnmarshalText implements the encoding.TextUnmarshaler interface.

type MismatchError

type MismatchError struct {
	A Amount
	B Amount
}

MismatchError is returned when two amounts have mismatched currency codes.

func (MismatchError) Error

func (e MismatchError) Error() string

type RoundingMode

type RoundingMode uint8

RoundingMode determines how the amount will be rounded.

const (
	// RoundHalfUp rounds up if the next digit is >= 5.
	RoundHalfUp RoundingMode = iota
	// RoundHalfDown rounds up if the next digit is > 5.
	RoundHalfDown
	// RoundUp rounds away from 0.
	RoundUp
	// RoundDown rounds towards 0, truncating extra digits.
	RoundDown
	// RoundHalfEven rounds up if the next digit is > 5. If the next digit is equal
	// to 5, it rounds to the nearest even decimal. Also called bankers' rounding.
	RoundHalfEven
)

Jump to

Keyboard shortcuts

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