moneylib
A library to deal with money safely using fixed-precision floating-point decimal.
Overview
moneylib provides a safe, robust, and ergonomic way to work with monetary value in Rust.
It handles currency and amount with operations and arithmetics avoiding floating, rounding, and precision issue exist in typical binary floating-point type.
It also make sure the money always in valid state on every operations and arithmetics done on it avoiding overflow/truncation/wrap and without fractions.
This crate uses Decimal type underneath for the amount of money.
More detailed docs:
Features
Here are some features supported:
- Type-safe:
- Compile-time check for arithmetics and operations.
- Runtime check for overflowed/wrapped/truncated amount.
- Prevents currencies mixing at compile-time.
- Value type to represent money.
Money: represents money in amount rounded to the currency's minor unit.RawMoney: represents money in raw amount keeping the precisions and choose when to round.
- Helper macros:
dec!(...): re-export from Decimal crate to instantiate hardcoded decimals.money!(...,...): instantiateMoneywith currency code and amount.raw!(...,...): instantiateRawMoneywith currency code and amount.
- Access to its amount and currency's metadata.
- Arithmetics: (*,/,+,-), operator overloading supported.
- Comparisons: (>,<,>=,<=,==,!=), operator overloading supported.
- Negative money.
- Formatting and custom formatting.
- Rounding with multiple strategies: Bankers rounding, half-up, half-down, ceil, and floor.
- Money in form of its smallest amount (minor amount).
- Some basic operations like absolute value, min, max, and clamp.
- Support for all ISO 4217 currencies.
- New/custom currency by implementing
Currencytrait. - Serde.
- Supports locale formatting.
- Exchange rates for conversions.
- Split and Allocation.
- Some accounting operations:
- Percentage calculations.
- Interest calculations.
- etc.
Example
use ;
// --- Creating money ---
// Use the money! macro for the most concise syntax (no `use moneylib::iso::USD` required)
let price = money!; // Money<USD>
let tax = money!;
// Or construct explicitly; amount is auto-rounded to the currency's minor unit
let rounded = new.unwrap; // USD 10.00 (bankers rounding)
// From a string with thousand separators
let parsed = from_code_comma_thousands.unwrap;
// From minor units (cents for USD: 12345 cents = USD 123.45)
let from_cents = from_minor.unwrap;
// --- Arithmetic (operator overloading; panics on overflow) ---
let total = price + tax; // USD 108.50
let half = total / dec!; // USD 54.25
// Safe (non-panicking) variants return Option
let safe_sum = price.checked_add.unwrap; // USD 108.50
// --- Comparisons ---
assert!;
assert!;
// --- Currency-specific minor units ---
let jpy = money!; // JPY 1,000 (0 decimal places)
let bhd = new.unwrap; // BHD 12.345 (3 decimal places)
// --- Formatting ---
let large = money!;
println!; // USD 1,234.56
println!; // $1,234.56
// --- Rounding strategies ---
let raw = from_decimal;
println!; // 123.46
println!; // 123.45
// --- Negative amounts ---
let negative = money!;
println!; // USD 50.00
// --- Percentage operations ---
let discount = price.percent.unwrap; // 10% of USD 100.00 = USD 10.00
let after_vat = price.percent_add.unwrap; // USD 100.00 + 20% = USD 120.00
// --- Iterator helpers (sum, mean) ---
let basket = vec!;
let sum = basket.checked_sum.unwrap; // USD 60.00
let mean = basket.mean.unwrap; // USD 20.00
// --- Split and allocate ---
let bill = money!;
let = bill.split.unwrap; // base = USD 3.33, remainder = USD 0.01
let parts: = bill.split.unwrap; // [USD 5.00, USD 3.00, USD 2.00]
// --- Compile-time currency type safety ---
let usd = money!;
let eur = money!;
// let is_same = usd == eur; // won't compile, as money with different currencies are treated as different types.
// usd + eur; // ← compile error: cannot add Money<USD> and Money<EUR>
Components
This library provides these main components to work with money:
Money<C>: represents the money itself and all operations on it. Generic over currency typeC.Currency: trait that defines currency behavior and metadata. Implemented by currency marker types (e.g.,USD,EUR,JPY).Decimal: 128 bit floating-point with fixed-precision decimal number. Re-export from rust_decimal represents main type for money's amount.BaseMoney: trait of money providing core operations and accessors.BaseOps: trait for arithmetic and comparison operations on money.IterOps: trait with blanket implementations for checked_sum, mean, median, and mode.CustomMoney: trait for custom formatting and rounding operations on money.RoundingStrategy: enum defining rounding strategies (BankersRounding, HalfUp, HalfDown, Ceil, Floor).MoneyError: enum of possible errors that can occur in money operations.
Money<C> and Decimal are Copy types so they can be passed around freely without having to worry about borrow checker.
Currency marker types are zero-sized types (ZST) for compile-time type safety.
Invariants
Monetary values are sensitive matter and their invariants must always hold true.
Decimal
- Significand(m): -296 < m < 296
- Decimal points(s): 0 <= s <= 28
Money
- Always rounded to its currency's minor unit using bankers rounding after each creation and operation done on it.
- Creating money from string only accepts currencies already defined in ISO 4217.
- Comparisons: Currency type-safety is enforced at compile time. Operations between different currencies won't compile.
- Arithmetics:
- *,+,-: will PANIC if overflowed. Currency mismatches are prevented at compile time.
- /: will PANIC if overflowed or division by zero. Currency mismatches are prevented at compile time.
- Use methods in
BaseOpsfor non-panic arithmetics.
Currency
- Currency trait defines properties for a currency, implemented by types denoting currencies inside
moneylib::isomodule. - All ISO 4217 currencies are supported.
- Currency information is available through
BaseMoneymethods:code(),symbol(),name(),numeric_code(),minor_unit(). - New/custom currency is supported by implementing the trait.
This library maintains type-safety by preventing invalid state either by returning Result/Option or going PANIC.
Feature Flags
raw_money
Enables the RawMoney<C> type which doesn't do automatic rounding like Money<C> does.
It keeps full decimal precision and lets callers decide when to round.
[]
= { = "...", = ["raw_money"] }
use ;
// RawMoney preserves all decimal precision
let raw = new.unwrap;
assert_eq!; // Not rounded!
// Convert from Money using into_raw()
let money = new.unwrap;
let raw = money.into_raw;
// Perform precise calculations
let result = raw * dec!; // Apply tax
// Convert back to Money with rounding using finish()
let final_money = result.finish;
Where rounding happens:
.round(): rounds to currency's minor unit using bankers rounding. ReturnsRawMoney..round_with(...): rounds using custom decimal points and strategy. ReturnsRawMoney..finish(): rounds to currency's minor unit using bankers rounding back toMoney.
serde
Enables serialization and deserialization for Money/RawMoney(raw_money) types.
By default it will serialize/deserialize as numbers from numbers or from string numbers.
If you want to serialize/deserialize as string money format with code or symbol, you can use provided serde interface inside serde module:
moneylib::serde::money::comma_str_code: Serialize into code format(e.g. "USD 1,234.56") with separators from currency's setting. Deserialize with code formatted with comma separated thousands.moneylib::serde::money::option_comma_str_code: Same as above, with nullability.moneylib::serde::money::comma_str_symbol: Serialize into symbol format(e.g. "$1,234.56") with separators from currency's setting. Deserialize with symbol formatted with comma separated thousands.moneylib::serde::money::option_comma_str_symbol: Same as above, with nullability.moneylib::serde::money::dot_str_code: Serialize into code format(e.g. "EUR 1.234,56") with separators from currency's setting. Deserialize with code formatted with dot separated thousands.moneylib::serde::money::option_dot_str_code: Same as above, with nullability.moneylib::serde::money::dot_str_symbol: Serialize into symbol format(e.g. "€1,234.56") with separators from currency's setting. Deserialize with symbol formatted with dot separated thousands.moneylib::serde::money::option_dot_str_symbol: Same as above, with nullability.
[]
= { = "...", = ["serde"] }
or serde for RawMoney:
[]
= { = "...", = ["serde", "raw_money"] }
use ;
use ;
let json_str = r#"
{
"amount_from_f64": 1234.56988,
"amount_from_i64": -1234,
"amount_from_u64": 18446744073709551615,
"amount_from_i128": -1844674407370955161588,
"amount_from_u128": 34028236692093846346337,
"amount_from_str": "1234.56",
"raw_amount_from_f64": -1004.1234,
"raw_amount_from_str": "1230.4993",
"amount_from_str_comma_code": "USD 1,234.56",
"amount_from_str_comma_code_some": "USD 2,000.00",
"amount_from_str_comma_code_none": null,
"amount_from_str_comma_symbol": "$1,234.56",
"amount_from_str_comma_symbol_some": "$2,345.6799",
"amount_from_str_comma_symbol_none": null,
"raw_amount_from_str_comma_code": "USD -42.42424242",
"amount_from_str_dot_code": "EUR 1.234,5634",
"amount_from_str_dot_code_some": "EUR 2.000,00",
"amount_from_str_dot_code_none": null,
"amount_from_str_dot_symbol": "€1.234,56",
"amount_from_str_dot_symbol_some": "€2.345,67",
"amount_from_str_dot_symbol_none": null,
"raw_amount_from_str_dot_symbol": "-€69,69696969"
}
"#;
let all = ;
dbg!;
assert!;
let ret = all.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
// comma + code
assert_eq!;
assert!;
assert_eq!;
assert!;
assert!;
// comma + symbol
assert_eq!;
assert!;
// "$2,345.6799" -> rounded to 2 decimal places -> 2345.68
assert_eq!;
assert!;
assert_eq!;
assert!;
// dot + code (European formatting)
// "EUR 1.234,5634" -> 1234.5634 -> rounded to 1234.56 (third decimal is 3 -> round down)
assert_eq!;
assert!;
assert_eq!;
assert!;
assert!;
// dot + symbol
assert_eq!;
assert!;
assert_eq!;
assert!;
assert!;
assert_eq!;
locale
Enable locale formatting using ISO 639 lowercase language code, ISO 639 with ISO 3166-1 alpha‑2 uppercase region code, and also supports BCP 47 locale extensions.
[]
= { = "...", = ["locale"] }
or locale for RawMoney:
[]
= { = "...", = ["locale", "raw_money"] }
use ;
use dec;
use MoneyFormatter;
// English (US) locale: comma thousands separator, dot decimal separator
let money = new.unwrap;
assert_eq!;
// Arabic (Saudi Arabia) locale: Arabic-Indic numerals
let money = new.unwrap;
assert_eq!;
// Negative amount: include `n` in format_str to show the negative sign
let money = new.unwrap;
assert_eq!;
// Indian numbers and group formatting.
let money = -new.unwrap;
let result = money.format_locale_amount;
assert_eq!;
// Invalid locale returns an error
let money = new.unwrap;
assert!;
exchange
Enable currency conversion feature with exchange rates.
Main Components:
Exchange: Trait with blanket implementation for convert method for types implementingBaseMoney<C>.ExchangeRates: Struct containing list of exchange rates with base currency.
[]
= { = "...", = ["exchange"] }
or exchange for RawMoney:
[]
= { = "...", = ["exchange", "raw_money"] }
use ;
let money = new.unwrap;
let ret = money.;
assert_eq!;
let money = new.unwrap;
let ret = money.;
assert_eq!;
let money = from_decimal;
let ret = money.;
assert_eq!;
let raw_money = from_decimal;
let ret = raw_money.;
assert_eq!;
let money = new.unwrap;
let mut rates = default;
assert_eq!;
assert_eq!;
rates.set;
rates.set;
assert_eq!;
let ret = money.;
assert_eq!;
let ret = money.;
assert_eq!;
let rates = from;
assert_eq!;
assert_eq!;
assert_eq!;
let money = from_decimal;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
accounting
Contains several features:
- Interest calculations(FV, PV, PMT).
[]
= { = "...", = ["accounting"] }