Expand description
§moneylib
A library to deal with money safely using floating-point fixed-precision 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.
§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.
- Some accounting operations:
- Percentage calculations.
- Interest calculations.
- etc.
§Example
use moneylib::{Money, BaseMoney, BaseOps, CustomMoney, RoundingStrategy, iso::{USD, JPY, BHD, EUR}, macros::dec};
use std::str::FromStr;
// Creating money from string (supports thousand separators)
let usd_money = Money::<USD>::from_str("USD 1,234.56").unwrap();
println!("{}", usd_money); // USD 1,234.56
// Creating money from minor amount (cents for USD)
let from_cents = Money::<USD>::from_minor(12345).unwrap();
println!("{}", from_cents); // USD 123.45
// Arithmetic operations with automatic rounding
let money_a = Money::<USD>::new(dec!(100.00)).unwrap();
let money_b = Money::<USD>::new(dec!(50.00)).unwrap();
println!("{}", money_a + money_b); // USD 150.00
println!("{}", money_a * dec!(1.5)); // USD 150.00
println!("{}", money_a / dec!(3)); // USD 33.33 (rounded)
// Comparisons
println!("{}", money_a > money_b); // true
println!("{}", money_a == Money::<USD>::new(dec!(100.00)).unwrap()); // true
// Working with different currencies
// JPY has 0 decimal places
let jpy_money = Money::<JPY>::new(dec!(1000)).unwrap();
println!("{}", jpy_money); // JPY 1,000
// BHD has 3 decimal places
let bhd_money = Money::<BHD>::new(dec!(12.345)).unwrap();
println!("{}", bhd_money); // BHD 12.345
// Custom formatting
let money = Money::<USD>::new(dec!(1234.56)).unwrap();
println!("{}", money.format_symbol()); // $1,234.56
println!("{}", money.format_code()); // USD 1,234.56
// Rounding with round_with method
let rounded = Money::<USD>::new(dec!(123.456)).unwrap();
let half_up_rounded = rounded.round_with(2, RoundingStrategy::HalfUp);
println!("{}", half_up_rounded.amount()); // 123.46
// Negative amounts
let negative = Money::<USD>::new(dec!(-50.00)).unwrap();
println!("{}", negative); // USD -50.00
println!("{}", negative.abs()); // USD 50.00
// Error handling with Result types
match money_a.checked_add(money_b) {
Some(sum) => println!("Sum: {}", sum),
None => println!("overflowed"),
}
// Safe operations with different currencies (won't compile due to type safety)
let eur_money = Money::<EUR>::new(dec!(100.00)).unwrap();
// This won't compile because USD and EUR are different types:
// let result = money_a + eur_money; // Compile error!§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): -2^96 < m < 2^96
- 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 types are defined at compile time using marker types (e.g.,
USD,EUR,JPY). - All ISO 4217 currencies are supported via the
Currencytrait. - Currency information is available through trait methods:
code(),symbol(),name(),minor_unit(). - New/custom currency is supported by implementing
Currencytrait.
This library maintains type-safety by preventing invalid state either by returning Result 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.
[dependencies]
moneylib = { version = "...", features = ["raw_money"] }use moneylib::{BaseMoney, RawMoney, iso::USD, Money, macros::dec};
// RawMoney preserves all decimal precision
let raw = RawMoney::<USD>::new(dec!(100.567)).unwrap();
assert_eq!(raw.amount(), dec!(100.567)); // Not rounded!
// Convert from Money using into_raw()
let money = Money::<USD>::new(dec!(100.50)).unwrap();
let raw = money.into_raw();
// Perform precise calculations
let result = raw * dec!(1.08875); // 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.
[dependencies]
moneylib = { version = "...", features = ["serde"] }or serde for RawMoney:
[dependencies]
moneylib = { version = "...", features = ["serde", "raw_money"] }use moneylib::{BaseMoney, Money, RawMoney, macros::dec};
use moneylib::iso::{CAD, EUR, GBP, IDR, JPY, USD};
#[derive(Debug, ::serde::Serialize, ::serde::Deserialize)]
struct All {
amount_from_f64: Money<USD>,
// `default` must be declared if you want to let users omit this field giving it money with zero amount.
#[serde(default)]
amount_from_f64_omit: Money<IDR>,
// `default` must be declared if you want to let users omit this field giving it money with zero amount.
#[serde(default)]
amount_from_str_omit: Money<CAD>,
amount_from_i64: Money<EUR>,
amount_from_u64: Money<USD>,
amount_from_i128: Money<USD>,
amount_from_u128: Money<USD>,
amount_from_str: Money<USD>,
raw_amount_from_f64: RawMoney<USD>,
raw_amount_from_str: RawMoney<USD>,
#[serde(with = "moneylib::serde::money::comma_str_code")]
amount_from_str_comma_code: Money<USD>,
#[serde(with = "moneylib::serde::money::option_comma_str_code")]
amount_from_str_comma_code_some: Option<Money<USD>>,
#[serde(with = "moneylib::serde::money::option_comma_str_code")]
amount_from_str_comma_code_none: Option<Money<USD>>,
// `default` must be declared if you want to let users omit this field making it `None`.
#[serde(with = "moneylib::serde::money::option_comma_str_code", default)]
amount_from_str_comma_code_omit: Option<Money<USD>>,
#[serde(with = "moneylib::serde::money::comma_str_symbol")]
amount_from_str_comma_symbol: Money<USD>,
#[serde(with = "moneylib::serde::money::option_comma_str_symbol")]
amount_from_str_comma_symbol_some: Option<Money<USD>>,
#[serde(with = "moneylib::serde::money::option_comma_str_symbol")]
amount_from_str_comma_symbol_none: Option<Money<USD>>,
// `default` must be declared if you want to let users omit this field making it `None`.
#[serde(with = "moneylib::serde::money::option_comma_str_symbol", default)]
amount_from_str_comma_symbol_omit: Option<Money<USD>>,
#[serde(with = "moneylib::serde::raw_money::comma_str_code")]
raw_amount_from_str_comma_code: RawMoney<USD>,
// dot
#[serde(with = "moneylib::serde::money::dot_str_code")]
amount_from_str_dot_code: Money<EUR>,
#[serde(with = "moneylib::serde::money::option_dot_str_code")]
amount_from_str_dot_code_some: Option<Money<EUR>>,
#[serde(with = "moneylib::serde::money::option_dot_str_code")]
amount_from_str_dot_code_none: Option<Money<EUR>>,
// `default` must be declared if you want to let users omit this field making it `None`.
#[serde(with = "moneylib::serde::money::option_dot_str_code", default)]
amount_from_str_dot_code_omit: Option<Money<EUR>>,
#[serde(with = "moneylib::serde::money::dot_str_symbol")]
amount_from_str_dot_symbol: Money<EUR>,
#[serde(with = "moneylib::serde::money::option_dot_str_symbol")]
amount_from_str_dot_symbol_some: Option<Money<EUR>>,
#[serde(with = "moneylib::serde::money::option_dot_str_symbol")]
amount_from_str_dot_symbol_none: Option<Money<EUR>>,
// `default` must be declared if you want to let users omit this field making it `None`.
#[serde(with = "moneylib::serde::money::option_dot_str_symbol", default)]
amount_from_str_dot_symbol_omit: Option<Money<EUR>>,
#[serde(with = "moneylib::serde::raw_money::dot_str_symbol")]
raw_amount_from_str_dot_symbol: RawMoney<EUR>,
}
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 = serde_json::from_str::<All>(json_str);
dbg!(&all);
assert!(all.is_ok());
let ret = all.unwrap();
assert_eq!(ret.amount_from_f64.amount(), dec!(1234.57));
assert_eq!(ret.amount_from_f64_omit.amount(), dec!(0));
assert_eq!(ret.amount_from_str_omit.amount(), dec!(0));
assert_eq!(ret.amount_from_i64.amount(), dec!(-1234));
assert_eq!(ret.amount_from_u64.amount(), dec!(18446744073709551615));
assert_eq!(ret.amount_from_i128.amount(), dec!(-1844674407370955161588));
assert_eq!(ret.amount_from_u128.amount(), dec!(34028236692093846346337));
assert_eq!(ret.amount_from_str.amount(), dec!(1234.56));
assert_eq!(ret.raw_amount_from_f64.amount(), dec!(-1004.1234,));
assert_eq!(ret.raw_amount_from_str.amount(), dec!(1230.4993));
// comma + code
assert_eq!(ret.amount_from_str_comma_code.amount(), dec!(1234.56));
assert!(ret.amount_from_str_comma_code_some.is_some());
assert_eq!(
ret.amount_from_str_comma_code_some
.as_ref()
.unwrap()
.amount(),
dec!(2000.00)
);
assert!(ret.amount_from_str_comma_code_none.is_none());
assert!(ret.amount_from_str_comma_code_omit.is_none());
// comma + symbol
assert_eq!(ret.amount_from_str_comma_symbol.amount(), dec!(1234.56));
assert!(ret.amount_from_str_comma_symbol_some.is_some());
// "$2,345.6799" -> rounded to 2 decimal places -> 2345.68
assert_eq!(
ret.amount_from_str_comma_symbol_some
.as_ref()
.unwrap()
.amount(),
dec!(2345.68)
);
assert!(ret.amount_from_str_comma_symbol_none.is_none());
assert_eq!(ret.raw_amount_from_str_comma_code.amount(), dec!(-42.42424242));
assert!(ret.amount_from_str_comma_symbol_omit.is_none());
// dot + code (European formatting)
// "EUR 1.234,5634" -> 1234.5634 -> rounded to 1234.56 (third decimal is 3 -> round down)
assert_eq!(ret.amount_from_str_dot_code.amount(), dec!(1234.56));
assert!(ret.amount_from_str_dot_code_some.is_some());
assert_eq!(
ret.amount_from_str_dot_code_some.as_ref().unwrap().amount(),
dec!(2000.00)
);
assert!(ret.amount_from_str_dot_code_none.is_none());
assert!(ret.amount_from_str_dot_code_omit.is_none());
// dot + symbol
assert_eq!(ret.amount_from_str_dot_symbol.amount(), dec!(1234.56));
assert!(ret.amount_from_str_dot_symbol_some.is_some());
assert_eq!(
ret.amount_from_str_dot_symbol_some
.as_ref()
.unwrap()
.amount(),
dec!(2345.67)
);
assert!(ret.amount_from_str_dot_symbol_none.is_none());
assert!(ret.amount_from_str_dot_symbol_omit.is_none());
assert_eq!(ret.raw_amount_from_str_dot_symbol.amount(), dec!(-69.69696969));§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.
[dependencies]
moneylib = { version = "...", features = ["locale"] }or locale for RawMoney:
[dependencies]
moneylib = { version = "...", features = ["locale", "raw_money"] }use moneylib::{BaseMoney, Money, Currency, iso::{USD, EUR, INR}};
use moneylib::macros::dec;
use moneylib::CustomMoney;
// English (US) locale: comma thousands separator, dot decimal separator
let money = Money::<USD>::new(dec!(1234.56)).unwrap();
assert_eq!(money.format_locale_amount("en-US", "c na").unwrap(), "USD 1,234.56");
// Arabic (Saudi Arabia) locale: Arabic-Indic numerals
let money = Money::<USD>::new(dec!(1234.56)).unwrap();
assert_eq!(money.format_locale_amount("ar-SA", "c na").unwrap(), "USD ١٬٢٣٤٫٥٦");
// Negative amount: include `n` in format_str to show the negative sign
let money = Money::<USD>::new(dec!(-1234.56)).unwrap();
assert_eq!(money.format_locale_amount("en-US", "c na").unwrap(), "USD -1,234.56");
// Indian numbers and group formatting.
let money = -Money::<INR>::new(dec!(1234012.52498)).unwrap();
let result = money.format_locale_amount("hi-IN-u-nu-deva", "s na");
assert_eq!(result.unwrap(), "₹ -१२,३४,०१२.५२");
// Invalid locale returns an error
let money = Money::<USD>::new(dec!(1234.56)).unwrap();
assert!(money.format_locale_amount("!!!invalid", "c na").is_err());
§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.
[dependencies]
moneylib = { version = "...", features = ["exchange"] }or exchange for RawMoney:
[dependencies]
moneylib = { version = "...", features = ["exchange", "raw_money"] }use moneylib::{
BaseMoney, Currency, Exchange, ExchangeRates, Money, RawMoney,
iso::{CAD, EUR, IDR, IRR, USD},
macros::dec,
};
let money = Money::<USD>::new(123).unwrap();
let ret = money.convert::<EUR>(dec!(0.8));
assert_eq!(ret.unwrap().amount(), dec!(98.4));
let money = Money::<USD>::new(123).unwrap();
let ret = money.convert::<USD>(2);
assert_eq!(ret.unwrap().amount(), dec!(123));
let money = Money::<USD>::from_decimal(dec!(100));
let ret = money.convert::<EUR>(0.888234);
assert_eq!(ret.unwrap().amount(), dec!(88.82));
let raw_money = RawMoney::<USD>::from_decimal(dec!(100));
let ret = raw_money.convert::<EUR>(0.8882346);
assert_eq!(ret.unwrap().amount(), dec!(88.82346));
let money = Money::<USD>::new(123).unwrap();
let mut rates = ExchangeRates::<USD>::default();
assert_eq!(rates.len(), 1);
assert_eq!(rates.get(USD::CODE).unwrap(), dec!(1));
rates.set(EUR::CODE, dec!(0.8));
rates.set(IDR::CODE, 17_000);
assert_eq!(rates.base(), "USD");
let ret = money.convert::<EUR>(&rates);
assert_eq!(ret.unwrap().amount(), dec!(98.4));
let ret = money.convert::<IDR>(&rates);
assert_eq!(ret.unwrap().amount(), dec!(2_091_000));
let rates = ExchangeRates::<EUR>::from([
("IDR", dec!(21_250)),
("IRR", dec!(1_652_125)),
("USD", dec!(1.25)),
("EUR", dec!(0.8)), // will be ignored since base already in eur and forced into 1.
]);
assert_eq!(rates.base(), "EUR");
assert_eq!(rates.len(), 4);
assert_eq!(rates.get(EUR::CODE).unwrap(), dec!(1));
let money = Money::<USD>::from_decimal(dec!(1000));
assert_eq!(money.convert::<USD>(&rates).unwrap().amount(), dec!(1000));
assert_eq!(money.convert::<EUR>(&rates).unwrap().amount(), dec!(800));
assert_eq!(
money.convert::<IRR>(&rates).unwrap().amount(),
dec!(1_321_700_000)
);
assert_eq!(
money.convert::<IDR>(&rates).unwrap().amount(),
dec!(17_000_000)
);§accounting
Contains several features:
- Interest calculations(FV, PV, PMT).
[dependencies]
moneylib = { version = "...", features = ["accounting"] }Modules§
- accounting
- Accounting module
- iso
- Contains all ISO 4217 currencies.
- macros
- Contains helper macros.
- serde
- Serde implementations
Macros§
- dec
- Creates a
Decimalvalue from a numeric literal. - money
- Creates a
Moneyinstance using a currency type and a decimal amount. - raw
- Creates a
RawMoneyinstance using a currency type and a decimal amount.
Structs§
- Decimal
Decimalrepresents a 128 bit representation of a fixed-precision decimal number. The finite set of values of typeDecimalare of the form m / 10e, where m is an integer such that -296 < m < 296, and e is an integer between 0 and 28 inclusive.- Exchange
Rates - Contains list of rates with a Base currency.
- Money
- Represents a monetary value with a specific currency and amount.
- RawMoney
- Represents a monetary value without automatic rounding.
Enums§
- Money
Error - Error type for moneylib.
- Rounding
Strategy - Defines the strategy for rounding decimal money amounts.
Traits§
- Base
Money - Base trait for all money types in the library.
- BaseOps
- Trait for arithmetic and comparison operations on money values.
- Currency
- The Currency properties
- Custom
Money - Trait for customizing money formatting and rounding behavior.
- Exchange
- Trait for currency exchange. This does exchange from C into T.
- IterOps
- Trait for statistical and aggregate operations on collections of money values.
- Percent
Ops