Package bcd
provides a Binary Coded Decimal (BCD) implementation for precise decimal arithmetic in Go. It's particularly useful for financial and currency calculations where floating-point errors are unacceptable.
- Exact Decimal Arithmetic: No floating-point rounding errors
- Generic API: Single function handles all numeric types with compile-time safety
- Currency Support: Built-in support for ISO 4217 currency codes
- Multiple Rounding Modes: Including banker's rounding (round half to even)
- Arbitrary Precision: Handle very large and very small numbers
- Currency Allocation: Split amounts without losing pennies
- International Format Parsing: Parse various currency formats (e.g., $1,234.56 or €1.234,56)
go get tideland.dev/go/bcd
Note: This package requires Go 1.18 or later for generic support.
package main
import (
"fmt"
"tideland.dev/go/bcd"
)
func main() {
// Create BCD numbers from any numeric type
a, _ := bcd.New("123.45") // from string
b, _ := bcd.New(67.89) // from float64
c, _ := bcd.New(100) // from int
// Perform exact arithmetic
sum := a.Add(b)
product := a.Mul(b)
quotient, _ := a.Div(b, 4, bcd.RoundHalfUp)
fmt.Println("Sum:", sum) // Sum: 191.34
fmt.Println("Product:", product) // Product: 8381.0205
}
// Create currency amounts from any numeric type
price, _ := bcd.NewCurrency("19.99", "USD") // from string
tax, _ := bcd.NewCurrency(1.60, "USD") // from float
shipping := bcd.MustNewCurrency(5, "USD") // from int
// Calculate total
subtotal, _ := price.Add(tax)
total, _ := subtotal.Add(shipping)
fmt.Println("Total:", total) // Total: $26.59
// Parse formatted currency strings
amount, _ := bcd.ParseCurrency("$1,234.56")
fmt.Println(amount) // $1234.56
// With float64
fmt.Println(0.1 + 0.2) // 0.30000000000000004
// With BCD
a, _ := bcd.New("0.1")
b, _ := bcd.New("0.2")
fmt.Println(a.Add(b)) // 0.3
// Split $100 among 3 people - works with any numeric type
bill := bcd.MustNewCurrency(100, "USD") // from int
shares, _ := bill.Split(3)
// Results:
// Person 1: $33.34
// Person 2: $33.33
// Person 3: $33.33
// Total: $100.00 (no pennies lost!)
The BCD
type represents a decimal number using binary coded decimal encoding:
New[T Numeric](value T, opts ...Option) (*BCD, error)
- Create from any numeric typeMust[T Numeric](value T, opts ...Option) *BCD
- Create and panic on errorZero() *BCD
- Create zero value- Options:
WithScale(int)
,WithRounding(RoundingMode)
for float conversions
The Currency
type wraps BCD with currency-specific features:
NewCurrency[T CurrencyNumeric](value T, code string, opts ...Option) (*Currency, error)
- Create from any numeric typeNewCurrencyMinor[T IntegerType](minorUnits T, code string) (*Currency, error)
- Create from minor units (cents)MustNewCurrency[T CurrencyNumeric](value T, code string, opts ...Option) *Currency
- Create and panic on errorParseCurrency(string) (*Currency, error)
- Parse formatted currency string
a.Add(b) // Addition
a.Sub(b) // Subtraction
a.Mul(b) // Multiplication
a.Div(b, scale, rounding) // Division with scale and rounding
a.Mod(b) // Modulo
a.Abs() // Absolute value
a.Neg() // Negation
curr1.Add(curr2) // Add (same currency only)
curr1.Sub(curr2) // Subtract (same currency only)
curr.Mul(factor) // Multiply by BCD
curr.MulInt64(n) // Multiply by integer
curr.Div(divisor) // Divide by BCD
curr.DivInt64(n) // Divide by integer
The package supports multiple rounding modes:
RoundDown
- Round towards zero (truncate)RoundUp
- Round away from zeroRoundHalfUp
- Round to nearest, ties away from zeroRoundHalfDown
- Round to nearest, ties towards zeroRoundHalfEven
- Round to nearest, ties to even (banker's rounding)RoundCeiling
- Round towards positive infinityRoundFloor
- Round towards negative infinity
value := bcd.Must("1.2350") // Use Must for known-good values
rounded := value.Round(2, bcd.RoundHalfEven) // 1.24
// Or with float input and options
value2, _ := bcd.New(1.2350, bcd.WithScale(4))
rounded2 := value2.Round(2, bcd.RoundHalfEven) // 1.24
Distribute amounts without losing pennies:
// Split evenly - using generic API
total := bcd.MustNewCurrency(100, "USD") // from int
parts, _ := total.Split(3)
// Results: $33.34, $33.33, $33.33
// Allocate by ratios
rent := bcd.MustNewCurrency(2000.00, "USD") // from float
shares, _ := rent.Allocate([]int{1, 2, 2}) // 1:2:2 ratio
// Results: $400.00, $800.00, $800.00
The package includes built-in support for major world currencies:
- Fiat: USD, EUR, GBP, JPY, CHF, CAD, AUD, CNY, and many more
- Crypto: BTC, ETH (with 8 decimal places)
- Precious metals: XAU (gold), XAG (silver), XPT (platinum), XPD (palladium)
Each currency has the correct number of decimal places (e.g., 2 for USD, 0 for JPY).
Parse various international currency formats:
amounts := []string{
"$1,234.56", // US format
"€1.234,56", // European format
"CHF 2'500.00", // Swiss format
"($50.00)", // Negative (accounting)
"¥1,234", // Japanese (no decimals)
}
for _, s := range amounts {
curr, _ := bcd.ParseCurrency(s)
fmt.Println(curr)
}
// You can also create currencies directly with the generic API
usd := bcd.MustNewCurrency(1234.56, "USD")
eur := bcd.MustNewCurrency("1234.56", "EUR")
jpy := bcd.MustNewCurrency(1234, "JPY")
The package defines several error types:
ErrDivisionByZero
- Division by zero attemptedErrInvalidFormat
- Invalid decimal string formatErrOverflow
- Arithmetic overflowErrUnknownCurrency
- Unknown currency codeErrCurrencyMismatch
- Operation on different currencies
BCD arithmetic is slower than native floating-point operations but provides exact decimal arithmetic. Use BCD when:
- Accuracy is more important than speed
- Working with money or financial calculations
- Decimal precision must be maintained
- Rounding behavior must be predictable
The generic API provides a clean, type-safe interface:
// Create from any numeric type
a := bcd.Must("123.45") // string
b := bcd.Must(123) // int
c := bcd.Must(123.45) // float64
d := bcd.Must(int32(123)) // int32
e := bcd.Must(uint64(123)) // uint64
// Type safety at compile time
// bcd.Must(complex(1, 2)) // Compile error!
// Currency with type inference
price1 := bcd.MustNewCurrency("19.99", "USD")
price2 := bcd.MustNewCurrency(19.99, "USD")
price3 := bcd.MustNewCurrency(1999, "JPY") // No decimals for JPY
// Minor units (cents) with any integer type
cents := bcd.MustNewCurrencyMinor(1999, "USD") // $19.99
pence := bcd.MustNewCurrencyMinor(uint32(1999), "GBP") // £19.99
See the examples_test.go file for comprehensive examples including:
- Generic API usage patterns
- Basic arithmetic operations
- Currency calculations
- Invoice generation
- International pricing
- Allocation and splitting
- Various rounding modes
This project is licensed under the BSD License - see the LICENSE file for details.
- Frank Mueller (https://github.com/themue / https://github.com/tideland / https://themue.dev)