https://mfirhas.com/rss.xml

moneylib

First Release: 09 Feb 2026
Status: active

page documentation source

About

moneylib provides a safe, robust, and ergonomic library for handling monetary values in Rust. It manages both currency and amount, utilizing internal logic that eliminates the rounding and precision errors common in binary floating-point types. By ensuring that every operation maintains a valid state, moneylib guarantees that money remains consistent and accurate.

Under the hood, this crate leverages the Decimal type to represent the underlying amount.

Why a Dedicated Money Type?

Monetary values require a dedicated type that abstracts the complexities of currency attributes, arithmetic and operations.

Many existing libraries in Rust and other ecosystems perform currency validation at runtime. This means every operation incurs a check, and as an application scales, the risk of mixing currencies increases. Furthermore, many of these libraries perform arithmetic using standard operators that can lead to silent wrapping or truncation if the values exceed their bounds. Because these libraries often treat different currencies as the same underlying object type, invariants can be easily violated when values are passed throughout a system.

In other ecosystems, some libraries require currency objects to be instantiated separately, adding boilerplate before you can even begin working with the money itself. In languages with manual memory management or with pointers, developers are often left to handle thread safety themselves when passing money objects by reference.

The moneylib Advantage

moneylib represents money as a value type that can be cheaply copied and passed around without the overhead of pointers or references.

  • Compile-Time Safety: Currencies are encoded at the type level. All arithmetic and operations are verified at compile-time, making it impossible to accidentally add USD to EUR. All money types with different currencies are treated as different currencies.

  • Runtime Safety: Unlike libraries that allow risky overflow or truncation, moneylib ensures that every calculation results in a mathematically sound and valid state.

  • Zero-Cost Abstractions: By leveraging Rust’s static analysis and powerful type system, these safety guarantees are enforced without any runtime performance penalty.

  • Formatting with custom format: allowing you to choose the positioning of code/symbol and negative sign, along with minor formatting.

Main Components

BaseMoney<C>

Base trait for monetary value. It contains attributes and formatting for money types. You need to put this trait in scope for usage. This trait is implemented by Money<C> and RawMoney<C>.

Examples

Money<C>

use moneylib::{BaseMoney, Money, RoundingStrategy, dec, iso::USD};

let money = Money::<USD>::new(1234.5567).unwrap();
assert_eq!(money.amount(), dec!(1234.56));
assert_eq!(money.round().amount(), dec!(1234.56));
assert_eq!(
    money.round_with(1, RoundingStrategy::Floor).amount(),
    dec!(1234.5)
);
assert_eq!(money.truncate().amount(), dec!(1234.0)); // truncated into 123, and 123 == 123.0
assert_eq!(money.truncate_with(1).amount(), dec!(1234.5));
assert_eq!(money.name(), "United States dollar");
assert_eq!(money.symbol(), "$");
assert_eq!(money.code(), "USD");
assert_eq!(money.numeric_code(), 840);
assert_eq!(money.minor_unit(), 2);
assert_eq!(money.minor_amount().unwrap(), 123456);
assert_eq!(money.thousand_separator(), ",");
assert_eq!(money.decimal_separator(), ".");
assert_eq!(money.is_zero(), false);
assert_eq!(money.is_positive(), true);
assert_eq!(money.is_negative(), false);
assert_eq!(money.fraction(), dec!(0.56));
assert_eq!(money.scale(), 2);
assert_eq!(money.mantissa(), 123456);
assert_eq!(money.format_code().as_str(), "USD 1,234.56");
assert_eq!(money.format_symbol().as_str(), "$1,234.56");
assert_eq!(money.format_code_minor().as_str(), "USD 123,456 ¢");
assert_eq!(money.format_symbol_minor().as_str(), "$123,456 ¢");
assert_eq!(money.display().as_str(), "USD 1,234.56")

RawMoney<C>

use moneylib::{BaseMoney, RawMoney, RoundingStrategy, dec, iso::USD};

let money = RawMoney::<USD>::new(1234.5567).unwrap();
assert_eq!(money.amount(), dec!(1234.5567));
assert_eq!(money.round().amount(), dec!(1234.56));
assert_eq!(
    money.round_with(1, RoundingStrategy::Floor).amount(),
    dec!(1234.5)
);
assert_eq!(money.truncate().amount(), dec!(1234.0)); // truncated into 123, and 123 == 123.0
assert_eq!(money.truncate_with(1).amount(), dec!(1234.5));
assert_eq!(money.name(), "United States dollar");
assert_eq!(money.symbol(), "$");
assert_eq!(money.code(), "USD");
assert_eq!(money.numeric_code(), 840);
assert_eq!(money.minor_unit(), 2);
assert_eq!(money.minor_amount().unwrap(), 123456);
assert_eq!(money.thousand_separator(), ",");
assert_eq!(money.decimal_separator(), ".");
assert_eq!(money.is_zero(), false);
assert_eq!(money.is_positive(), true);
assert_eq!(money.is_negative(), false);
assert_eq!(money.fraction(), dec!(0.5567));
assert_eq!(money.scale(), 4);
assert_eq!(money.mantissa(), 12345567);
assert_eq!(money.format_code().as_str(), "USD 1,234.5567");
assert_eq!(money.format_symbol().as_str(), "$1,234.5567");
assert_eq!(money.format_code_minor().as_str(), "USD 123,456 ¢");
assert_eq!(money.format_symbol_minor().as_str(), "$123,456 ¢");
assert_eq!(money.display().as_str(), "USD 1,234.5567")

Currency

Trait for moneys currencies. This trait defines constant attributes for a currency, and all currency must implement this trait. This trait is re-export from currencylib. The currencylib also re-export the ISO 4217 currencies ready to use. This trait is type of generic parameter for many types.

The trait definition:

pub trait Currency {
    const CODE: &'static str;
    const SYMBOL: &'static str;
    const NAME: &'static str;
    const NUMERIC: u16;
    const MINOR_UNIT: u16;
    const MINOR_UNIT_SYMBOL: &'static str;
    const THOUSAND_SEPARATOR: &'static str;
    const DECIMAL_SEPARATOR: &'static str;
}

Example of ISO 4217 Currency from the list:

use moneylib::{Currency, iso::{USD /* ...and other iso 4217 currencies you want use, e.g. EUR, CAD, etc*/};
use std::str::FromStr;

assert_eq!(USD::CODE, "USD");
assert_eq!(USD::SYMBOL, "$");
assert_eq!(USD::NAME, "United States dollar");
assert_eq!(USD::MINOR_UNIT, 2);
assert_eq!(USD::MINOR_UNIT_SYMBOL, "¢");
assert_eq!(USD::NUMERIC, 840_u16);
assert_eq!(USD::THOUSAND_SEPARATOR, ",");
assert_eq!(USD::DECIMAL_SEPARATOR, ".");

let usd = USD::from_str("USD").unwrap();
assert_eq!(usd, USD);

The existing iso currencies are imported from module iso.

The existing currencies implementations are all zero-cost and zero-sized types that only exist at compile-time.

If you want to create new currency or customize a currency, you can implement this trait such:

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct BTC;

impl moneylib::Currency for BTC {
    const CODE: &'static str = "BTC";
    const SYMBOL: &'static str = "";
    const NAME: &'static str = "Bitcoin";
    const NUMERIC: u16 = 123;
    const MINOR_UNIT: u16 = 8;
    const MINOR_UNIT_SYMBOL: &'static str = "satoshi";
    const THOUSAND_SEPARATOR: &'static str = ",";
    const DECIMAL_SEPARATOR: &'static str = ".";
}

...and use it:

use moneylib::{BaseMoney, money, dec, raw};

let btc = money!(self::BTC, 5);
assert_eq!(btc.amount(), dec!(5));
assert_eq!(btc.minor_amount().unwrap(), 500_000_000);
assert_eq!(btc.format_code().as_str(), "BTC 5.00000000");
assert_eq!(btc.format_symbol().as_str(), "₿5.00000000");
assert_eq!(btc.format_code_minor().as_str(), "BTC 500,000,000 satoshi");
assert_eq!(btc.format_symbol_minor().as_str(), "₿500,000,000 satoshi");

let btc_raw = raw!(self::BTC, 2.003298789);
assert_eq!(btc_raw.amount(), dec!(2.003298789));
assert_eq!(btc_raw.minor_amount().unwrap(), 200_329_879);
assert_eq!(btc_raw.format_code().as_str(), "BTC 2.003298789");
assert_eq!(btc_raw.format_symbol().as_str(), "₿2.003298789");
assert_eq!(
    btc_raw.format_code_minor().as_str(),
    "BTC 200,329,879 satoshi"
);
assert_eq!(
    btc_raw.format_symbol_minor().as_str(),
    "₿200,329,879 satoshi"
);

Money<C>

Concrete type representing tender money that always rounded to its minor unit. The rounding happen at every arithmetic and operations according to its currency minor unit. Use this concrete type everywhere you need to represent money along with its constructions.

RawMoney<C>

Requires feature raw_money

Concrete type representing non-rounded money where precisions keep at every arithmetic and operations. This let you choose when, where and how to round. Use this concrete type everywhere you expect raw moneys. It can be constructed from this type, or converted from Money<C>.

Decimal

Concrete type representing monetary amount using crate rust_decimal and re-exported so you don't have to import rust_decimal. This type is a fixed-precision floating-point decimal type for Rust, so you don't have to worry about binary floating-point precision issue. From the crate doc:

Decimal represents a 128 bit representation of a fixed-precision decimal number. The finite set of values of type Decimal are of the form m / 10^e, where m is an integer such that -2^96 < m < 2^96, and e is an integer between 0 and 28 inclusive.

BaseOps<C>

Base trait for arithmetic and operations for monetary value type. This trait also extend operator overloading for +, -, *, /, +=, -=, *=, /=, % and negative sign for money(-). Operator overload arithmetic can be panicked at runtime because the nature of it not returning Result/Option.

For non-panic arithmetic and operations, BaseOps<C> provides:

  • is_approx: equality with tolerance(inclusive).
  • abs: get absolute value.
  • checked_add: non-panic addition.
  • checked_sub: non-panic substraction.
  • checked_mul: non-panic multiplication.
  • checked_div: non-panic division.
  • checked_rem: non-panic remainder.
  • split: split money without losing a single penny.
  • split_dist: split and distribute remainder equally across parts.
  • allocate: allocate money by percentages.
  • allocate_by_ratios: allocate money by ratios.

Types and Traits

Here are all types and traits provide to use alongside the main components:

  • PercentOps<T> trait: percentage operations.
  • IterOps<T> trait: operations on iterable types.
  • MoneyFormatter<C> trait: trait for custom formatting, including locale.
  • MoneyOps<C> trait: trait combining BaseOps<C> + MoneyFormatter<C> + PercentOps<C> + Exchange<C> + AccountingOps<C> This trait is shortcut for many traits for convenience, and the traits imported toggled by feature flags.
  • RoundingStrategy enum: rounding strategy for custom rounding.
  • MoneyError enum: error type.
  • Helper macros:
    • money!: create hardcoded Money<C> type.
    • raw!: create hardcoded RawMoney<C> type.
    • dec!: create hardcoded Decimal type.
  • Exchange trait: trait for currencies conversion.
  • ExchangeRates struct: contain rates for conversions.
  • iso module: contain all ISO 4217 currencies to use where Currency trait is expected.
  • accounting module: module containing all accounting operations.
  • serde module: module containing all serde implementations.

...or you can just import all of them via prelue:

use moneylib::prelude::*;
...

Constructors

You can construct types via concrete types: Money<C> and RawMoney<C>.

Money<C>

use moneylib::{
    BaseMoney, Money,
    iso::{CHF, IDR, USD},
};
use std::str::FromStr;

// using BaseMoney::new
let money = Money::<USD>::new(5000);
let money2 = Money::<USD>::new(5000);
assert!(money.is_ok());
assert!(money2.is_ok());
assert_eq!(money.unwrap(), money2.unwrap());

let money = Money::<USD>::new(5000.34456).unwrap();
let money2 = Money::<USD>::new(5000.34455).unwrap();
assert_eq!(money.amount(), dec!(5_000.34));
assert_eq!(money2.amount(), dec!(5_000.34));
assert_eq!(money, money2);

let money = Money::<USD>::new(5000.34456).unwrap();
let money2 = Money::<USD>::new(5000.34655).unwrap();
assert_eq!(money.amount(), dec!(5_000.34));
assert_eq!(money2.amount(), dec!(5_000.35));
assert_ne!(money, money2);

let overflow_money = Money::<USD>::new(i128::MAX);
assert!(overflow_money.is_err());

// using from_decimal
use moneylib::{Decimal, macros::dec};
let money = Money::<USD>::from_decimal(Decimal::from(10_000)); // USD 10,000.00
let money2 = Money::<USD>::from_decimal(dec!(10000.002)); // USD 10,000.00
assert_eq!(money, money2);

// using from_minor
let money = Money::<USD>::from_minor(1_340_201).unwrap(); // USD 13,402.01
let money2 = Money::<USD>::from_decimal(dec!(13_402.01)); // USD 13,402.01
assert_eq!(money, money2);

// constructing from string with codes and symbols format

// from string amount
let money: Money<USD> = Money::from_str("1234.556").unwrap();
assert_eq!(money.amount(), dec!(1_234.56));

// from string with code, comma thousands separator and dot decimal separator.
// using comma for thousands separator is optional.
let money: Money<USD> = Money::from_code_comma_thousands("USD 1,234.555").unwrap();
assert_eq!(money.amount(), dec!(1234.56));
let money: Money<USD> = Money::from_code_comma_thousands("USD 1234.555").unwrap();
assert_eq!(money.amount(), dec!(1234.56));
let money: Money<USD> = Money::from_code_comma_thousands("USD -1,234.555").unwrap();
assert_eq!(money.amount(), dec!(-1234.56));

// from string with code, dot thousands separator and comma decimal separator.
// using dot for thousands separator is optional.
let money: Money<IDR> = Money::from_code_dot_thousands("IDR 1.234,555").unwrap();
assert_eq!(money.amount(), dec!(1234.56));
let money: Money<IDR> = Money::from_code_dot_thousands("IDR 1234,555").unwrap();
assert_eq!(money.amount(), dec!(1234.56));
let money: Money<IDR> = Money::from_code_dot_thousands("IDR -1.234,555").unwrap();
assert_eq!(money.amount(), -dec!(1234.56));

// from string with symbol, comma thousands separator and dot decimal separator.
// using comma for thousands separator is optional.
let money: Money<USD> = Money::from_symbol_comma_thousands("$1,234.555").unwrap();
assert_eq!(money.amount(), dec!(1234.56));
let money: Money<USD> = Money::from_symbol_comma_thousands("$1234.555").unwrap();
assert_eq!(money.amount(), dec!(1234.56));
let money: Money<USD> = Money::from_symbol_comma_thousands("-$1,234.555").unwrap();
assert_eq!(money.amount(), dec!(-1234.56));

// from string with code, dot thousands separator and comma decimal separator.
// using dot for thousands separator is optional.
let money: Money<IDR> = Money::from_symbol_dot_thousands("Rp1.234,555").unwrap();
assert_eq!(money.amount(), dec!(1234.56));
let money: Money<IDR> = Money::from_symbol_dot_thousands("Rp1234,555").unwrap();
assert_eq!(money.amount(), dec!(1234.56));
let money: Money<IDR> = Money::from_symbol_dot_thousands("-Rp1.234,555").unwrap();
assert_eq!(money.amount(), -dec!(1234.56));

// parsing string with code using locale separators
let money: Money<CHF> = Money::from_code_locale_separator("CHF 1'000'234.028").unwrap();
assert_eq!(money.amount(), dec!(1_000_234.03));

// parsing string with symbol using locale separators
let money: Money<CHF> = Money::from_symbol_locale_separator("₣1'000'234.028").unwrap();
assert_eq!(money.amount(), dec!(1_000_234.03)); 

RawMoney<C>

Requires feature raw_money

use moneylib::{
    BaseMoney, RawMoney,
    iso::{CHF, IDR, USD},
};
use moneylib::{Decimal, macros::dec};
use std::str::FromStr;

// using BaseMoney::new
let money = RawMoney::<USD>::new(dec!(5000.34456)).unwrap();
let money2 = RawMoney::<USD>::new(dec!(5000.34455)).unwrap();
assert_ne!(money, money2);
assert_eq!(money.amount(), dec!(5000.34456));
assert_eq!(money2.amount(), dec!(5000.34455));

let money = RawMoney::<USD>::new(dec!(5000.346562343453456)).unwrap();
let money2 = RawMoney::<USD>::new(dec!(5000.346562343453456)).unwrap();
assert_eq!(money, money2);
assert_eq!(money.amount(), dec!(5000.346562343453456));
assert_eq!(money2.amount(), dec!(5000.346562343453456));

let overflow_money = RawMoney::<USD>::new(i128::MAX);
assert!(overflow_money.is_err());

// using from_decimal
let money = RawMoney::<USD>::from_decimal(Decimal::from(10_000)); // USD 10,000.00
let money2 = RawMoney::<USD>::from_decimal(dec!(10000.002)); // USD 10,000.002
assert_ne!(money, money2);

// using from_minor
let money = RawMoney::<USD>::from_minor(1_340_201).unwrap(); // USD 13,402.01
let money2 = RawMoney::<USD>::from_decimal(dec!(13_402.01)); // USD 13,402.01
assert_eq!(money, money2);

// constructing from string with codes and symbols format

// from string amount
let money: RawMoney<USD> = RawMoney::from_str("1234.556").unwrap();
assert_eq!(money.amount(), dec!(1_234.556));

// from string with code, comma thousands separator and dot decimal separator.
// using comma for thousands separator is optional.
let money: RawMoney<USD> = RawMoney::from_code_comma_thousands("USD 1,234.555").unwrap();
assert_eq!(money.amount(), dec!(1234.555));
let money: RawMoney<USD> = RawMoney::from_code_comma_thousands("USD 1234.555").unwrap();
assert_eq!(money.amount(), dec!(1234.555));
let money: RawMoney<USD> = RawMoney::from_code_comma_thousands("USD -1,234.555").unwrap();
assert_eq!(money.amount(), dec!(-1234.555));

// from string with code, dot thousands separator and comma decimal separator.
// using dot for thousands separator is optional.
let money: RawMoney<IDR> = RawMoney::from_code_dot_thousands("IDR 1.234,555").unwrap();
assert_eq!(money.amount(), dec!(1234.555));
let money: RawMoney<IDR> = RawMoney::from_code_dot_thousands("IDR 1234,555").unwrap();
assert_eq!(money.amount(), dec!(1234.555));
let money: RawMoney<IDR> = RawMoney::from_code_dot_thousands("IDR -1.234,555").unwrap();
assert_eq!(money.amount(), -dec!(1234.555));

// from string with symbol, comma thousands separator and dot decimal separator.
// using comma for thousands separator is optional.
let money: RawMoney<USD> = RawMoney::from_symbol_comma_thousands("$1,234.555").unwrap();
assert_eq!(money.amount(), dec!(1234.555));
let money: RawMoney<USD> = RawMoney::from_symbol_comma_thousands("$1234.555").unwrap();
assert_eq!(money.amount(), dec!(1234.555));
let money: RawMoney<USD> = RawMoney::from_symbol_comma_thousands("-$1,234.555").unwrap();
assert_eq!(money.amount(), dec!(-1234.555));

// from string with code, dot thousands separator and comma decimal separator.
// using dot for thousands separator is optional.
let money: RawMoney<IDR> = RawMoney::from_symbol_dot_thousands("Rp1.234,555").unwrap();
assert_eq!(money.amount(), dec!(1234.555));
let money: RawMoney<IDR> = RawMoney::from_symbol_dot_thousands("Rp1234,555").unwrap();
assert_eq!(money.amount(), dec!(1234.555));
let money: RawMoney<IDR> = RawMoney::from_symbol_dot_thousands("-Rp1.234,555").unwrap();
assert_eq!(money.amount(), -dec!(1234.555));

// parsing string with code using locale separators
let money: RawMoney<CHF> =
    RawMoney::from_code_locale_separator("CHF 1'000'234.028").unwrap();
assert_eq!(money.amount(), dec!(1_000_234.028));

// parsing string with symbol using locale separators
let money: RawMoney<CHF> =
    RawMoney::from_symbol_locale_separator("₣1'000'234.028").unwrap();
assert_eq!(money.amount(), dec!(1_000_234.028));

Decimal

You can construct decimal amount using re-exported Decimal type or macro dec!.

use moneylib::{Decimal, dec};
use std::str::FromStr;

let hardcoded_dec = dec!(123.34); // any rust number format works
let from_string = Decimal::from_str("123.34").unwrap();
let from = Decimal::from(123); // only integers
let try_from = Decimal::try_from(123.34).unwrap();
let try_from_i128 = Decimal::try_from(123_i128).unwrap();

assert_eq!(hardcoded_dec, from_string);
assert_eq!(from, try_from_i128);
assert_eq!(hardcoded_dec, try_from);
assert_eq!(from_string, try_from);
assert_ne!(hardcoded_dec, from);
assert_ne!(from_string, try_from_i128);

Generic Constructor

This is constructor from BaseMoney<C>::new type provide constructor for any type implementing it. This is useful especially if you want polymorphism on BaseMoney<C> type and want to construct the type, use BaseMoney<C> generic constructor: This constructor accepts amount from Decimal, i32, i64, i128, and f64. Another usecase is when you want to return generic money on BaseMoney<C>.

Careful when constructing money with amount in f64, as f64 can't represent some amounts the Decimal number can represents. It's preferable to supply Decimal or integer(if the amount has no fraction).

Macro Constructors

You can construct hardcoded moneys using helper macros. These macros wrap money types and currencies so you don't have to bring them in scope.

money!

use moneylib::{BaseMoney, dec, money};

// Money<C> and iso currencies don't need to be in scope.
let money = money!(USD, 42300.289);
assert_eq!(money.amount(), dec!(42300.29));

raw!

Requires feature raw_money

use moneylib::{BaseMoney, dec, raw};

// RawMoney<C> and iso currencies don't need to be in scope.
let money = raw!(USD, 42300.2323);
assert_eq!(money.amount(), dec!(42300.2323));

RawMoney<C> conversion

Requires feature raw_money

One good usecase for RawMoney<C> is when you need to keep precisions and avoid rounding process along calculations/arithmetic/operations. You can convert between Money<C> and RawMoney<C> back and forth.

Money<C> to RawMoney<C>

When raw_money feature enabled, there's new method for Money<C> called .into_raw() which convert it into RawMoney<C>.

use moneylib::{BaseMoney, BaseOps, RoundingStrategy, dec, money, raw};

// required `raw_money` feature enabled.
let money = money!(USD, 123_456.55);
let raw_money = money.into_raw();
assert_eq!(money.amount(), raw_money.amount());
assert_eq!(money.amount(), dec!(123_456.55));
assert_eq!(raw_money.amount(), dec!(123_456.55));

// .. do some calculations with raw_money,
let after_calc = raw_money.checked_mul(0.2343455).unwrap();
assert_eq!(after_calc.amount(), dec!(28931.486938025));

RawMoney<C> to Money<C>

After some calculation, you can keep it that way, or turn it back into tender money by using .finish(), :

...
// and convert it back to tender money
let money = after_calc.finish();
assert_eq!(money.amount(), dec!(28931.49));

let raw = raw!(USD, 45000.1289);
let trunc = raw.truncate_with(3);
assert_eq!(trunc.amount(), dec!(45000.128));
let round = trunc.round();
assert_eq!(round.amount(), dec!(45_000.13));
let round_1_scale = round.round_with(1, RoundingStrategy::Ceil);
assert_eq!(round_1_scale.amount(), dec!(45_000.2));
// convert to tender money
let money = round_1_scale.finish();
assert_eq!(money, money!(USD, 45_000.20));

Rounding

Money<C> always rounded to currency's minor unit because it's tender money. The rounding method for this is Banker's Rounding. If you want to keep precision and round where, when and how you like, you can use RawMoney<C>.

Here are several rounding methods provided for BaseMoney<C>::round_with method via RoundingStrategy enum:

  • BankersRounding: Round to half even.
  • HalfUp: Rounds half values away from zero.
  • HalfDown: Rounds half values toward zero.
  • Ceil: Rounds away from zero (toward positive/negative infinity).
  • Floor: Rounds toward zero (truncates).

Banker's rounding also happen when you want to get minor amount from RawMoney<C>. You can avoid this behavior by rounding it before calling .minor_amount() method on money.

Fallible

Result<T, MoneyError>

Result returns when the functions/methods are fallible on errors. The error type MoneyError contains several errors that might happen:

/// Error type for moneylib.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MoneyError {
    ParseStr,
    DecimalConversion,
    ArithmeticOverflow,
    CurrencyMismatch,

    #[cfg(feature = "locale")]
    ParseLocale,
}

Option<T>

Option returns for calculations and operations where the result overflowed. Other minor possibility is when the logic doesn't permit it to happen.

Default

Both Money<C> and RawMoney<C> implement Default trait returning zero amount money.

Absolute value

You can get absolute value for both Money<C> and RawMoney<C> from

use moneylib::{BaseOps, money};

let neg_money = money!(USD, -123);
let neg_money2 = -money!(USD, 123);
assert_eq!(neg_money.abs(), neg_money2.abs());

Requires feature raw_money

use moneylib::{BaseOps, raw};

let neg_money = raw!(USD, -123);
let neg_money2 = -raw!(USD, 123);
assert_eq!(neg_money.abs(), neg_money2.abs());

Comparison

Comparisons can be done through comparison operators: ==, <, >, <=, >= and BaseOps<C> is_approx method:

Operator Overloading

use moneylib::money;
use moneylib::{BaseOps, Money, dec};

let money = Money::<moneylib::iso::USD>::from_decimal(dec!(40_430_884.2299));
let money2 = money!(USD, 40_430_884.23);
let same = money == money2;
assert!(same);

let money3 = money!(EUR, 40_430_884.23);
// let same = money == money3; // compile error

let money4 = money!(EUR, 40_430_884.22);
let bigger = money3 > money4;
assert!(bigger);
let bigger = money3 >= money4;
assert!(bigger);

let smaller = money4 < money3;
assert!(smaller);
let smaller = money4 <= money3;
assert!(smaller);

Requires feature raw_money

use moneylib::raw;
use moneylib::{BaseOps, RawMoney, dec};

let money = RawMoney::<moneylib::iso::USD>::from_decimal(dec!(40_430_884.2299));
let money2 = raw!(USD, 40_430_884.2299);
let same = money == money2;
assert!(same);

let money3 = raw!(EUR, 40_430_884.2299);
// let same = money == money3; // compile error

let money4 = raw!(EUR, 40_430_884.22);
let bigger = money3 > money4;
assert!(bigger);
let bigger = money3 >= money4;
assert!(bigger);

let smaller = money4 < money3;
assert!(smaller);
let smaller = money4 <= money3;
assert!(smaller);

is_approx

Check equality with tolerance. The tolerance is inclusive.

use moneylib::money;
use moneylib::{BaseOps, dec};

// approximation comparison
let money = money!(USD, 400.01);
let money2 = money!(USD, 400);
let same = money.is_approx(money2, 0.01);
assert!(same);

Requires feature raw_money

use moneylib::raw;
use moneylib::{BaseOps, dec};

// approximation comparison
let money = raw!(USD, 400.0552);
let money2 = raw!(USD, 400.0520);
let same = money.is_approx(money2, 0.0033);
assert!(same);

Arithmetic

There are 2 ways for doing arithmetic: operator overloading and BaseOps. Operator Overloading does panic when calculation is overflow. BaseOps provide non-panic calculation returning Result or Option.

Operator Overloading

use moneylib::{BaseMoney, dec, money};

let money = money!(USD, 7239.422);
let money2 = money!(USD, 123.999);
let ret = money + money2;
let ret = ret - dec!(20);
let ret = (ret * dec!(5)) / dec!(2);
assert_eq!(ret.amount(), dec!(18_358.55));
let ret = ret - -money!(USD, 2);
assert_eq!(ret.amount(), dec!(18_360.55));

let mut m = money!(EUR, 350);
let m2 = money!(EUR, 2);
m += m2;
m -= money!(EUR, 5);
m *= money!(EUR, 0.2);
m /= money!(EUR, 4);
assert_eq!(m, money!(EUR, 17.35));

let money = money!(USD, 100);
let rem = money % dec!(3);
assert_eq!(rem.amount(), dec!(1));

For raw money:

Requires feature raw_money

use moneylib::{BaseMoney, dec, raw};

let money = raw!(USD, 7239.422);
let money2 = raw!(USD, 123.999);
let ret = money + money2;
let ret = ret - dec!(20);
let ret = (ret * dec!(5)) / dec!(2);
assert_eq!(ret.amount(), dec!(18_358.55250));
let ret = ret - raw!(USD, -2);
assert_eq!(ret.amount(), dec!(18_360.55250));

let mut m = raw!(EUR, 350);
let m2 = raw!(EUR, 2);
m += m2;
m -= raw!(EUR, 5);
m *= raw!(EUR, 0.2);
m /= raw!(EUR, 4);
assert_eq!(m, raw!(EUR, 17.35));

let money = raw!(USD, 100);
let rem = money % dec!(3);
assert_eq!(rem.amount(), dec!(1));

Negative sign:

You can flexibly put the sign in front of type or the amount.

use moneylib::money;
use moneylib::{BaseMoney, Money};

let money = Money::<moneylib::iso::USD>::new(-2500.4).unwrap();
let money2 = -Money::<moneylib::iso::USD>::new(2500.4).unwrap();
assert_eq!(money, money2);

let money = Money::<moneylib::iso::USD>::new(2500.4).unwrap();
let money2 = -Money::<moneylib::iso::USD>::new(-2500.4).unwrap();
assert_eq!(money, money2);

let money = money!(USD, -123_000.29);
let money2 = -money!(USD, 123_000.29);
assert_eq!(money, money2);

let money = money!(USD, 3_222);
let money2 = -money!(USD, -3_222);
assert_eq!(money, money2);

For raw money:

Requires feature raw_money

use moneylib::raw;
use moneylib::{BaseMoney, RawMoney};

let money = RawMoney::<moneylib::iso::USD>::new(-2500.444).unwrap();
let money2 = -RawMoney::<moneylib::iso::USD>::new(2500.444).unwrap();
assert_eq!(money, money2);

let money = RawMoney::<moneylib::iso::USD>::new(2500.488).unwrap();
let money2 = -RawMoney::<moneylib::iso::USD>::new(-2500.488).unwrap();
assert_eq!(money, money2);

let money = raw!(USD, -123_000.29);
let money2 = -raw!(USD, 123_000.29);
assert_eq!(money, money2);

let money = raw!(USD, 3_222);
let money2 = -raw!(USD, -3_222);
assert_eq!(money, money2);

Checked Arithmetic

use moneylib::{BaseMoney, BaseOps, dec, money};

let money = money!(USD, 6942.12);
let money2 = money!(USD, 4269.12);
let ret = money.checked_add(money2).unwrap();
assert_eq!(ret.amount(), dec!(11_211.24));

let ret = ret.checked_div(2).unwrap();
assert_eq!(ret.amount(), dec!(5605.62));

let ret = ret.checked_mul(2.5).unwrap();
assert_eq!(ret, money!(USD, 14014.05));

let ret = (ret.checked_sub(245).unwrap().checked_add(5000).unwrap())
    .checked_mul(1.5)
    .unwrap();
assert_eq!(ret, money!(USD, 28153.58));

let money = money!(USD, 100);
let rem = money.checked_rem(3).unwrap();
assert_eq!(rem.amount(), dec!(1));

For raw money:

Requires feature raw_money

use moneylib::{BaseMoney, BaseOps, dec, raw};

let money = raw!(USD, 6942.121233);
let money2 = raw!(USD, 4269.1240);
let ret = money.checked_add(money2).unwrap();
assert_eq!(ret.amount(), dec!(11211.245233));

let ret = ret.checked_div(2).unwrap();
assert_eq!(ret.amount(), dec!(5605.6226165));

let ret = ret.checked_mul(2.5).unwrap();
assert_eq!(ret, raw!(USD, 14014.056541250,));

let ret = (ret.checked_sub(245).unwrap().checked_add(5000).unwrap())
    .checked_mul(1.5)
    .unwrap();
assert_eq!(ret, raw!(USD, 28153.5848118750));

let money = raw!(USD, 100);
let rem = money.checked_rem(3).unwrap();
assert_eq!(rem.amount(), dec!(1));

Split & Allocation

split

Split money without losing a single penny returning equal split and remainder(if any).

use moneylib::{BaseMoney, BaseOps, dec, money};

let money = money!(USD, 100);
let split = 3;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each, money!(USD, 33.33));
assert!(!remainder.is_zero());
assert_eq!(remainder.amount(), dec!(0.01));

let money = -money!(USD, 100);
let split = 3;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each, -money!(USD, 33.33));
assert!(!remainder.is_zero());
assert_eq!(remainder.amount(), -dec!(0.01));

let money = money!(USD, 0);
let split = 3;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each, money!(USD, 0));
assert_eq!(remainder.amount(), dec!(0));

let money = money!(USD, 100);
let split = 0;
let ret = money.split(split);
assert!(ret.is_none());

let money = money!(USD, 100);
let split = 1;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each.amount(), dec!(100));
assert!(remainder.is_zero());

let money = money!(USD, 100);
let split = 4;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each.amount(), dec!(25));
assert!(remainder.is_zero());

For raw money:

Requires feature raw_money

Raw money splitting make sure the digits not rounded up so we can get the remainder. This is because of calculation that results in long precision after decimal point.
use moneylib::{BaseMoney, BaseOps, dec, raw};

let money = raw!(USD, 0.0001);
let split = 3;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each, raw!(USD, 0.0000333333333333333333333333));
assert!(!remainder.is_zero());
assert_eq!(remainder.amount(), dec!(0.0000000000000000000000000001));

let money = -raw!(USD, 0.0001);
let split = 3;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each, -raw!(USD, 0.0000333333333333333333333333));
assert!(!remainder.is_zero());
assert_eq!(remainder.amount(), -dec!(0.0000000000000000000000000001));

let money = raw!(USD, 0);
let split = 3;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each, raw!(USD, 0));
assert_eq!(remainder.amount(), dec!(0));

let money = raw!(USD, 0.0001);
let split = 0;
let ret = money.split(split);
assert!(ret.is_none());

let money = raw!(USD, 0.0001);
let split = 1;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each.amount(), dec!(0.0001));
assert!(remainder.is_zero());

let money = raw!(USD, 0.0001);
let split = 4;
let (each, remainder) = money.split(split).unwrap();
assert_eq!(each.amount(), dec!(0.000025));
assert!(remainder.is_zero());

split_dist

Split money without losing a single penny returning vector of equal parts where remainder distributed across parts.

use moneylib::{BaseOps, Money, iso::USD, money};

let money = money!(USD, 100);
let split = 3;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<Money<USD>>(), money);
assert_eq!(
    &parts,
    &[money!(USD, 33.34), money!(USD, 33.33), money!(USD, 33.33)]
);

let money = -money!(USD, 100);
let split = 3;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<Money<USD>>(), money);
assert_eq!(
    &parts,
    &[
        -money!(USD, 33.34),
        money!(USD, -33.33),
        -money!(USD, 33.33)
    ]
);

let money = money!(USD, 0);
let split = 3;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<Money<USD>>(), money);
assert_eq!(&parts, &[money!(USD, 0), money!(USD, 0), money!(USD, 0)]);

let money = money!(USD, 100);
let split = 0;
let parts = money.split_dist(split);
assert!(parts.is_none());

let money = money!(USD, 100);
let split = 1;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<Money<USD>>(), money);
assert_eq!(&parts, &[money!(USD, 100)]);

let money = money!(USD, 100);
let split = 4;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<Money<USD>>(), money);
assert_eq!(
    &parts,
    &[
        money!(USD, 25),
        money!(USD, 25),
        money!(USD, 25),
        money!(USD, 25)
    ]
);

For raw money:

Requires feature raw_money

use moneylib::{BaseOps, RawMoney, iso::USD, raw};

let money = raw!(USD, 100);
let split = 3;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<RawMoney<USD>>(), money);
assert_eq!(
    &parts,
    &[
        raw!(USD, 33.33333333333333333333333334),
        raw!(USD, 33.33333333333333333333333333),
        raw!(USD, 33.33333333333333333333333333)
    ]
);

let money = -raw!(USD, 100);
let split = 3;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<RawMoney<USD>>(), money);
assert_eq!(
    &parts,
    &[
        -raw!(USD, 33.33333333333333333333333334),
        raw!(USD, -33.33333333333333333333333333),
        -raw!(USD, 33.33333333333333333333333333)
    ]
);

let money = raw!(USD, 0);
let split = 3;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<RawMoney<USD>>(), money);
assert_eq!(&parts, &[raw!(USD, 0), raw!(USD, 0), raw!(USD, 0)]);

let money = raw!(USD, 100);
let split = 0;
let parts = money.split_dist(split);
assert!(parts.is_none());

let money = raw!(USD, 100);
let split = 1;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<RawMoney<USD>>(), money);
assert_eq!(&parts, &[raw!(USD, 100)]);

let money = raw!(USD, 100);
let split = 4;
let parts = money.split_dist(split).unwrap();
assert_eq!(parts.len(), split as usize);
assert_eq!(parts.iter().sum::<RawMoney<USD>>(), money);
assert_eq!(
    &parts,
    &[raw!(USD, 25), raw!(USD, 25), raw!(USD, 25), raw!(USD, 25)]
);

allocate

Allocate money by percentages. Percentages must sum into 100.

use moneylib::{BaseOps, IterOps, money};

let money = money!(BHD, 101.001);
let allocations = money.allocate(&[30, 30, 30, 10]).unwrap();
let expected = &[
    money!(BHD, 30.301),
    money!(BHD, 30.300),
    money!(BHD, 30.300),
    money!(BHD, 10.100),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = money!(JPY, 101);
let allocations = money.allocate(&[30, 30, 30, 10]).unwrap();
let expected = &[
    money!(JPY, 31),
    money!(JPY, 30),
    money!(JPY, 30),
    money!(JPY, 10),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = money!(JPY, 0.6); // rounded to 1
let allocations = money.allocate(&[30, 30, 30, 10]).unwrap();
let expected = &[
    money!(JPY, 1),
    money!(JPY, 0),
    money!(JPY, 0),
    money!(JPY, 0),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = money!(USD, 500);
let allocations = money.allocate(&[25, 25, 25, 25]).unwrap();
let expected = &[
    money!(USD, 125),
    money!(USD, 125),
    money!(USD, 125),
    money!(USD, 125),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

For raw money:

Requires feature raw_money

use moneylib::{BaseOps, IterOps, raw};

let money = raw!(BHD, 101.001);
let allocations = money.allocate(&[30, 30, 30, 10]).unwrap();
let expected = &[
    raw!(BHD, 30.30030),
    raw!(BHD, 30.30030),
    raw!(BHD, 30.30030),
    raw!(BHD, 10.10010),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = -raw!(JPY, 101);
let allocations = money.allocate(&[30, 30, 30, 10]).unwrap();
let expected = &[
    -raw!(JPY, 30.30),
    -raw!(JPY, 30.30),
    raw!(JPY, -30.30),
    raw!(JPY, -10.10),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = raw!(JPY, 0.6);
let allocations = money.allocate(&[30, 30, 30, 10]).unwrap();
let expected = &[
    raw!(JPY, 0.180),
    raw!(JPY, 0.180),
    raw!(JPY, 0.180),
    raw!(JPY, 0.060),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = raw!(USD, 79.228162514264337593543950335);
let ret = money.allocate(&[10, 10, 80]).unwrap();
let expected = &[
    raw!(USD, 7.922816251426433759354395035),
    raw!(USD, 7.922816251426433759354395035),
    raw!(USD, 63.382530011411470074835160265),
];
assert_eq!(&ret, expected);
assert_eq!(ret.checked_sum().unwrap(), money);

allocate_by_ratios

Allocate money by ratios.

use moneylib::{BaseOps, IterOps, money};

let money = money!(BHD, 100.001);
let allocations = money.allocate_by_ratios(&[1, 1, 1]).unwrap();
let expected = &[
    money!(BHD, 33.334),
    money!(BHD, 33.334),
    money!(BHD, 33.333),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = -money!(JPY, 100);
let allocations = money.allocate_by_ratios(&[1, 1, 1]).unwrap();
let expected = &[-money!(JPY, 34), -money!(JPY, 33), money!(JPY, -33)];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = money!(JPY, 351);
let allocations = money.allocate_by_ratios(&[2, 1, 1]).unwrap();
let expected = &[money!(JPY, 175), money!(JPY, 88), money!(JPY, 88)];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = money!(USD, 100);
let ret = money.allocate_by_ratios(&[1, 1, 1]).unwrap();
let expected = &[money!(USD, 33.34), money!(USD, 33.33), money!(USD, 33.33)];
assert_eq!(&ret, expected);
assert_eq!(ret.checked_sum().unwrap(), money);

For raw money:

Requires feature raw_money

use moneylib::{BaseOps, IterOps, raw};

let money = raw!(BHD, 100.001);
let allocations = money.allocate_by_ratios(&[1, 1, 1]).unwrap();
let expected = &[
    raw!(BHD, 33.33366666666666666666666667),
    raw!(BHD, 33.33366666666666666666666667),
    raw!(BHD, 33.33366666666666666666666666),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = -raw!(JPY, 100);
let allocations = money.allocate_by_ratios(&[1, 1, 1]).unwrap();
let expected = &[
    -raw!(JPY, 33.33333333333333333333333334),
    raw!(JPY, -33.33333333333333333333333333),
    raw!(JPY, -33.33333333333333333333333333),
];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = raw!(JPY, 351);
let allocations = money.allocate_by_ratios(&[2, 1, 3]).unwrap();
let expected = &[raw!(JPY, 117), raw!(JPY, 58.50), raw!(JPY, 175.50)];
assert_eq!(&allocations, &expected);
assert_eq!(allocations.checked_sum().unwrap(), money);

let money = raw!(USD, 100);
let ret = money.allocate_by_ratios(&[1, 1, 1]).unwrap();
let expected = &[
    raw!(USD, 33.33333333333333333333333334),
    raw!(USD, 33.33333333333333333333333333),
    raw!(USD, 33.33333333333333333333333333),
];
assert_eq!(&ret, expected);
assert_eq!(ret.checked_sum().unwrap(), money);

Formatter

Formatting RawMoney requires feature raw_money

Formatting money into string for display. There are 2 formatter, one from BaseMoney<C> trait, and the other from MoneyFormatter<C> trait.

The ones from BaseMoney<C> format using predefined format with code and symbol along with minor amount formatting and with locale separators. MoneyFormatter<C> provides more custom way of formatting using custom format and locale. The locale supports ISO 639 lowercase language code, ISO 639 with ISO 3166-1 alpha‑2 uppercase region code, also support BCP 47 locale extensions.

BaseMoney<C> formatter

Formats money with code and symbol using locale separators listed here. Formatting with code adds space between code and amount. Formatting with symbol adds no space. Negative sign for code put before amount, and for symbol put before symbol.

use moneylib::{BaseMoney, money, raw};

// BaseMoney formatter
let money = money!(USD, 1234000.587);
assert_eq!(&money.format_code(), "USD 1,234,000.59");
assert_eq!(&money.format_symbol(), "$1,234,000.59");
assert_eq!(&money.format_code_minor(), "USD 123,400,059 ¢");
assert_eq!(&money.format_symbol_minor(), "$123,400,059 ¢");
assert_eq!(&money.display(), &money.format_code());

let money = -raw!(USD, 1234000.587);
assert_eq!(&money.format_code(), "USD -1,234,000.587");
assert_eq!(&money.format_symbol(), "-$1,234,000.587");
assert_eq!(&money.format_code_minor(), "USD -123,400,059 ¢");
assert_eq!(&money.format_symbol_minor(), "-$123,400,059 ¢");
assert_eq!(&money.display(), &money.format_code());

MoneyFormatter<C> format

This method adds customisation for formatting money allowing you to positions the display components. Each display component represent by a letter:

  • 'a': amount (displayed as absolute value)
  • 'c': currency code (e.g., "USD")
  • 's': currency symbol (e.g., "$")
  • 'm': minor symbol (e.g., "cents")
  • 'n': negative sign (-), only displayed when amount is negative

You can escape one of them in format string by using escape \. In Rust string, you'll write it like Tot\\al giving you literal "Total". Extra \ is a mechanism in Rust, it's like saying to escape the escape to escape the character next to it.

Another more readable approach to escaping or you want to write literal characters or text inside money formatting, is by using \{you string goes here}. In Rust string, you'll write it like \\{Total} giving you literal "Total".

Amount in formatting always display as absolute value, so it's preferable to use n for displaying negative money.

m always display money in minor amount.

use moneylib::{MoneyFormatter, money, raw};

let money = money!(USD, 100.50);

// Basic formatting
// "USD 100.50"
assert_eq!(money.format("c a"), "USD 100.50");

// "$100.50"
assert_eq!(money.format("sa"), "$100.50");

assert_eq!(money.format("c nsa"), "USD $100.50");

// "USD 10,050 ¢" (amount in minor units when 'm' is present)
assert_eq!(money.format("c a m"), "USD 10,050 ¢");

// adding `n` to positive money will be ignored
assert_eq!(money.format("c na"), "USD 100.50");

// Mixing literals with format symbols
// "Total: $100.50"
assert_eq!(money.format("Tot\\al: sa"), "Total: $100.50");

// Escaping format symbols to display them as literals
// "a=100.50, c=USD"
assert_eq!(money.format("\\a=a, \\c=c"), "a=100.50, c=USD");

let negative = -money!(USD, 50);
// "USD -50.00"
assert_eq!(negative.format("c na"), "USD -50.00");
// "-$50.00"
assert_eq!(negative.format("nsa"), "-$50.00");

// not specifying the `n` for negative sign will omit the negative sign.
assert_eq!(negative.format("sa"), "$50.00");

assert_eq!(
    money.format("\\{Total Price is:} n\\{US}s a"),
    "Total Price is: US$ 100.50"
);
assert_eq!(
    negative.format("\\{Groceries spending:} n\\{US}s a"),
    "Groceries spending: -US$ 50.00"
);

// raw money
let money = -raw!(IDR, 93009.446688);
let ret = money.format("c na");
assert_eq!(ret, "IDR -93.009,446688");

MoneyFormatter<C> format_with_separator

Same with format, only this allow you to customize the separators for thousands and decimal.

use moneylib::{MoneyFormatter, money, raw};

let money = money!(USD, 93009.446688);
let ret = money.format_with_separator("c na", "*", "#");
assert_eq!(ret, "USD 93*009#45");

let money = money!(CNY, 93009.446688);
let ret = money.format_with_separator("s na", " ", ",");
assert_eq!(ret, "¥ 93 009,45");

let money = raw!(IDR, -93009.446688);
let ret = money.format_with_separator("nc a", "*", "#");
assert_eq!(ret, "-IDR 93*009#446688");

let money = raw!(IDR, 93009.446688);
let ret = money.format_with_separator("s na", " ", ",");
assert_eq!(ret, "Rp 93 009,446688");

MoneyFormatter<C> format_locale_amount

Requires feature locale

Formats money amount with locale. The locale supports ISO 639 lowercase language code, ISO 639 with ISO 3166-1 alpha‑2 uppercase region code, also support BCP 47 locale extensions.

List of BCP 47 extension numbers support: icu4x (Look for "digits")

use moneylib::{MoneyFormatter, money, raw};

let money = money!(IDR, 123123123);
assert_eq!(
    money.format_locale_amount("id-ID", "nsa").unwrap(),
    "Rp123.123.123,00"
);

let money = money!(INR, 123123123);
assert_eq!(
    money.format_locale_amount("hi-IN", "s na").unwrap(),
    "₹ 12,31,23,123.00"
);

// Arabic (Saudi Arabia) locale: Arabic numerals
let money = money!(SAR, 1234.56);
assert_eq!(
    money.format_locale_amount("ar-SA", "c na").unwrap(),
    "SAR ١٬٢٣٤٫٥٦"
);

// Arabic (Persians) locale: Arabic numerals
let money = money!(IRR, 1234.56);
assert_eq!(
    money.format_locale_amount("fa", "c na").unwrap(),
    "IRR ۱٬۲۳۴٫۵۶"
);

// Negative amount: include `n` in format_str to show the negative sign
let money = money!(IRR, -1234.56);
assert_eq!(
    money.format_locale_amount("fa", "s na").unwrap(),
    "﷼ -۱٬۲۳۴٫۵۶"
);

// Indian numbers and group formatting.
let money = raw!(INR, -1234012.52498);
let result = money.format_locale_amount("hi-IN", "s na");
assert_eq!(result.unwrap(), "₹ -12,34,012.52498");

// with locale numbers using BCP 47 extension
let money = raw!(INR, -1234012.52498);
let result = money.format_locale_amount("hi-IN-u-nu-deva", "s na");
assert_eq!(result.unwrap(), "₹ -१२,३४,०१२.५२४९८");

let money = money!(CNY, 1234.56);
let result = money
    .format_locale_amount("zh-CN-u-nu-hanidec", "c na")
    .unwrap();
assert_eq!(result, "CNY 一,二三四.五六");

let money = money!(KRW, 1234.56);
let result = money.format_locale_amount("ko-KR", "s na").unwrap();
assert_eq!(result, "₩ 1,235");

let money = money!(JPY, 12349.22);
let result = money.format_locale_amount("ja-JP", "s na").unwrap();
assert_eq!(result, "¥ 12,349");

let money = raw!(BDT, 1234.56);
let result = money.format_locale_amount("bn-BD", "s na").unwrap();
assert_eq!(result, "৳ ১,২৩৪.৫৬");

let money = raw!(THB, 1234.56);
let result = money
    .format_locale_amount("th-TH-u-nu-thai", "s na")
    .unwrap();
assert_eq!(result, "฿ ๑,๒๓๔.๕๖");

// Invalid locale returns an error
let money = raw!(USD, 1234.56);
assert!(money.format_locale_amount("!!!invalid", "c na").is_err());

Iterable

Operations done on iterable types, e.g. Vec<T>, [T; n], &[T]. These types allow you to contain moneys of the same currencies.

These operations supported in 2 ways:

  • Sum operation using stdlib sum trait.
  • Operations from trait IterOps<C>

std::iter::Sum

Both Money<C> and RawMoney<C> implement this trait so you can sum all elements inside after spawning iter container type.

use moneylib::{Money, RawMoney, iso::EUR, money, raw};

let moneys = vec![
    money!(EUR, 1),
    money!(EUR, 100),
    money!(EUR, 123.32),
    money!(EUR, 30299.2234),
    money!(EUR, 2290.82668),
];

let sum = moneys.iter().sum::<Money<EUR>>();
assert_eq!(sum, money!(EUR, 32814.37));

let sum = moneys.into_iter().sum::<Money<EUR>>();
assert_eq!(sum, money!(EUR, 32814.37));

// raw money
let moneys = vec![
    raw!(EUR, 1),
    raw!(EUR, 100),
    raw!(EUR, 123.32),
    raw!(EUR, 30299.2234),
    raw!(EUR, 2290.82668),
];

let sum = moneys.iter().sum::<RawMoney<EUR>>();
assert_eq!(sum, raw!(EUR, 32814.37008));

let sum = moneys.into_iter().sum::<RawMoney<EUR>>();
assert_eq!(sum, raw!(EUR, 32814.37008));

IterOps<C>

This trait supports more operations such: checked_sum, mean, median, mode.

use moneylib::{IterOps, money, raw};

let moneys = vec![
    money!(EUR, 1),
    money!(EUR, 100),
    money!(EUR, 123.32),
    money!(EUR, 30299.2234),
    money!(EUR, 2290.82668),
];

let sum = moneys.checked_sum().unwrap();
assert_eq!(sum, money!(EUR, 32814.37));

// raw money
let moneys = vec![
    raw!(EUR, 1),
    raw!(EUR, 100),
    raw!(EUR, 123.32),
    raw!(EUR, 30299.2234),
    raw!(EUR, 2290.82668),
];

let sum = moneys.checked_sum().unwrap();
assert_eq!(sum, raw!(EUR, 32814.37008));
use moneylib::{IterOps, money, raw};

let moneys = vec![
    money!(EUR, 1),
    money!(EUR, 100),
    money!(EUR, 100),
    money!(EUR, 100),
    money!(EUR, 100),
    money!(EUR, 123.32),
    money!(EUR, 4500.32),
    money!(EUR, 4500.82),
    money!(EUR, 4500.92),
    money!(EUR, 4502.34),
    money!(EUR, 30299.2234),
    money!(EUR, 30299.2234),
    money!(EUR, 2290.82668),
];

let mean = moneys.mean().unwrap();
assert_eq!(mean, money!(EUR, 6262.92));

let median = moneys.median().unwrap();
assert_eq!(median, money!(EUR, 2290.83));

let modes = moneys.mode().unwrap();
assert_eq!(modes, vec![money!(EUR, 100)]);

// raw money
let moneys = vec![
    raw!(EUR, 1),
    raw!(EUR, 100),
    raw!(EUR, 100),
    raw!(EUR, 100),
    raw!(EUR, 100),
    raw!(EUR, 123.32),
    raw!(EUR, 4500.32),
    raw!(EUR, 4500.82),
    raw!(EUR, 4500.92),
    raw!(EUR, 4500.92),
    raw!(EUR, 4502.34),
    raw!(EUR, 30299.2234),
    raw!(EUR, 2290.82668),
];

let mean = moneys.mean().unwrap();
assert_eq!(mean, raw!(EUR, 4278.4376984615384615384615385));

let median = moneys.median().unwrap();
assert_eq!(median, raw!(EUR, 2290.82668));

let modes = moneys.mode().unwrap();
assert_eq!(modes, vec![raw!(EUR, 100)]);

Percentage

Do some percentage operations. Supported operations:

  • percent: Calculates what a certain percentage of a money amount equals.
  • percent_add: Adds amount by percentage
  • percent_adds_fixed: Adds self by multiple percentages from original amount.
  • percent_adds_compound: Adds self by multiple percentages compounding.
  • percent_sub: Substracts amount by percentage(discount)
  • percent_subs_sequence: Substracts self by multiple percentages in sequence.
  • percent_of: Determines what percentage one money is of another.
use moneylib::{BaseMoney, PercentOps, dec, money};

// money

// Calculate how much percentage of money equals to
let money = money!(USD, 200);
let tax = money.percent(15).unwrap(); // 15% of $200
assert_eq!(tax.amount(), dec!(30));
// Returns None on overflow
let none_on_overflow = money.percent(moneylib::Decimal::MAX);
assert!(none_on_overflow.is_none());

// Add money to a percentage of it
let price = money!(USD, 100);
let after_tax = price.percent_add(20).unwrap(); // $100 + 20% = $120
assert_eq!(after_tax.amount(), dec!(120));
// Returns None on overflow
let none_on_overflow = price.percent_add(moneylib::Decimal::MAX);
assert!(none_on_overflow.is_none());

// Add money to multiple percentages fixed to original amount.
let base = money!(USD, 1_000);
// All percentages are applied to the original base amount:
// $1000 + 10% of $1000 + 5% of $1000 = $1000 + $100 + $50 = $1150
let total = base.percent_adds_fixed([10, 5]).unwrap();
assert_eq!(total.amount(), dec!(1150));
// Returns None on overflow
let none_on_overflow = base.percent_adds_fixed([moneylib::Decimal::MAX]);
assert!(none_on_overflow.is_none());

// Add money to multiple percentages compounding.
let base = money!(USD, 1_000);
// Percentages compound on the running total:
// Step 1: $1000 + 10% of $1000  = $1100
// Step 2: $1100 + 5%  of $1100  = $1155
let total = base.percent_adds_compound([10, 5]).unwrap();
assert_eq!(total.amount(), dec!(1155));
// Returns None on overflow
let none_on_overflow = base.percent_adds_compound([moneylib::Decimal::MAX]);
assert!(none_on_overflow.is_none());

// Substract money by percentage of it
let price = money!(USD, 200);
let after_discount = price.percent_sub(25).unwrap(); // $200 - 25% = $150
assert_eq!(after_discount.amount(), dec!(150));
// Returns None on overflow
let none_on_overflow = price.percent_sub(moneylib::Decimal::MAX);
assert!(none_on_overflow.is_none());

// Substract money by multiple percentages in sequence.
let gross = money!(USD, 1_000);
// Deductions compound on the running total:
// Step 1: $1000 - 10% of $1000 = $900
// Step 2: $900  - 5%  of $900  = $855
let net = gross.percent_subs_sequence([10, 5]).unwrap();
assert_eq!(net.amount(), dec!(855));
// Returns None on overflow
let none_on_overflow = gross.percent_subs_sequence([moneylib::Decimal::MAX]);
assert!(none_on_overflow.is_none());

// Determines what percentage one money is of another.
let profit = money!(USD, 50);
let revenue = money!(USD, 200);
let margin_percentage = profit.percent_of(revenue).unwrap(); // $50 is 25% of $200
assert_eq!(margin_percentage, dec!(25));
// Returns None when dividing by zero
let zero = money!(USD, 0);
assert!(profit.percent_of(zero).is_none());

Exchange

Requires feature exchange

Exchange<From> trait provides conversion method(convert) from a currency into another currency.

The rate can be supplied from these:

  • Money<T> where T is target currency
  • RawMoney<T> where T is target currency
  • Decimal
  • f64
  • i32
  • i64
  • i128
  • ExchangeRates<'a, C> where C is base currency of exchange rates

ExchangeRates<'a, C> is a struct containing the exchange rates of multiple currencies with a base currency C. For example:

CurrenciesRatesPair
USD(base)1USD/USD
IDR17,000USD/IDR
EUR0.8USD/EUR
CAD1.2USD/CAD
JPY160USD/JPY

Base currency is always set to 1, and cannot change. When you set a new rate, or updating an existing one, you can only set non-base currency, and it always set the rate for Base/X, where X is your existing or new rate.

You can get the rate of a currency, relative from the base currency. You can also get the rate of pair currencies as long as the currencies are in the exchange rates.

The conversion can go from any currency to any currency as long as the currencies are in the rates.

use moneylib::{
    Currency, Exchange, ExchangeRates, Money, dec,
    iso::{CAD, CHF, EUR, IDR, IRR, SGD, USD},
    money,
};

// conversion with single rate

let usd = money!(USD, 5);
let usd_idr_rate = money!(IDR, 17_002);
let idr_from_5usd = usd.convert::<IDR>(usd_idr_rate).unwrap();
assert_eq!(idr_from_5usd, money!(IDR, 85010));

// convert to self, rate will be ignored since self will be returned immediately.
let usd = money!(USD, 5);
let another_usd = usd.convert::<USD>(50).unwrap();
assert_eq!(another_usd, money!(USD, 5));

let usd = money!(USD, 5);
let idr_from_5usd = usd.convert::<IDR>(0).unwrap();
assert_eq!(idr_from_5usd, money!(IDR, 0.00));

// conversion with exchange rates

// exchange rates contains rates of multiple currencies with one currency as the base.
// e.g.
// USD 1
// IDR 17,000
// EUR 0.8
// CAD 1.2
// SGD 1.4
// IRR 1,000,000
// CHF 0.5

let mut rates = ExchangeRates::<USD>::new();
rates.set(IDR::CODE, 17_000);
rates.set(EUR::CODE, 0.8);
rates.set(CAD::CODE, 1.2);
rates.set(SGD::CODE, 1.4);
rates.set(IRR::CODE, 1_000_000);
rates.set(CHF::CODE, 0.5);
rates.set(USD::CODE, 80); // ignored, since base already in USD, USD/USD stays 1.

let usd = money!(USD, 5);

let usd_chf = usd.convert::<CHF>(&rates).unwrap();
assert_eq!(usd_chf, money!(CHF, 2.5));

let usd_irr = usd.convert::<IRR>(&rates).unwrap();
assert_eq!(usd_irr, money!(IRR, 5_000_000));

// we can convert any currency to any currency as long as it exists in the rates.
let idr_usd = money!(IDR, 54_000_000).convert::<USD>(&rates).unwrap();
assert_eq!(idr_usd, money!(USD, 3176.47));

let cad_sgd: Money<SGD> = money!(CAD, 23480).convert(&rates).unwrap();
assert_eq!(cad_sgd, money!(SGD, 27393.33));

let eur_irr: Money<IRR> = money!(EUR, 254).convert(&rates).unwrap();
assert_eq!(eur_irr, money!(IRR, 317_500_000.00));

let hkd_idr: Option<Money<IDR>> = money!(HKD, 4598.33).convert(&rates);
assert!(hkd_idr.is_none()); // because HKD is not in the rates

// you can initialize the exchange rates like this:
let rates = ExchangeRates::<USD>::from([
    ("IDR", dec!(17_000)),
    ("EUR", dec!(0.8)),
    ("CAD", dec!(1.2)),
    ("SGD", dec!(1.4)),
    ("IRR", dec!(1_000_000)),
    ("CHF", dec!(0.5)),
    ("USD", dec!(80)), // will be ignored since base already in usd and forced into 1.
]);

let usd = money!(USD, 5);

let usd_chf = usd.convert::<CHF>(&rates).unwrap();
assert_eq!(usd_chf, money!(CHF, 2.5));

let usd_irr = usd.convert::<IRR>(&rates).unwrap();
assert_eq!(usd_irr, money!(IRR, 5_000_000));

// we can convert any currency to any currency as long as it exists in the rates.
let idr_usd = money!(IDR, 54_000_000).convert::<USD>(&rates).unwrap();
assert_eq!(idr_usd, money!(USD, 3176.47));

let cad_sgd: Money<SGD> = money!(CAD, 23480).convert(&rates).unwrap();
assert_eq!(cad_sgd, money!(SGD, 27393.33));

let eur_irr: Money<IRR> = money!(EUR, 254).convert(&rates).unwrap();
assert_eq!(eur_irr, money!(IRR, 317_500_000.00));

let hkd_idr: Option<Money<IDR>> = money!(HKD, 4598.33).convert(&rates);
assert!(hkd_idr.is_none()); // because HKD is not in the rates

JSON Serde

Requires feature serde

Serde is done using none other than serde crate.

JSON serde supports these methods:

  • default:
    • serialize into JSON number.
    • deserialize from string numbers or JSON numbers, supporting arbitrary precision.
  • comma_str_code:
    • serialize into string of <CODE> <AMOUNT> with comma as thousands separator and dot as decimal separator.
    • deserialize from string of <CODE> <AMOUNT> with comma as thousands separator and dot as decimal separator.
  • comma_str_symbol:
    • serialize into string of <SYMBOL><AMOUNT> with comma as thousands separator and dot as decimal separator.
    • deserialize from string of <SYMBOL><AMOUNT> with comma as thousands separator and dot as decimal separator.
  • dot_str_code:
    • serialize into string of <CODE> <AMOUNT> with dot as thousands separator and comma as decimal separator.
    • deserialize from string of <CODE> <AMOUNT> with dot as thousands separator and comma as decimal separator.
  • dot_str_symbol:
    • serialize into string of <SYMBOL><AMOUNT> with dot as thousands separator and comma as decimal separator.
    • deserialize from string of <SYMBOL><AMOUNT> with dot as thousands separator and comma as decimal separator.
  • str_code:
    • serialize into string of <CODE> <AMOUNT> with locale separators.
    • deserialize from string of <CODE> <AMOUNT> with locale separators.
  • str_symbol:
    • serialize into string of <SYMBOL><AMOUNT> with locale separators.
    • deserialize from string of <SYMBOL><AMOUNT> with locale separators.
  • minor:
    • serialize into signed integer of minor amount.
    • deserialize from signed integer of minor amount.
  • option_*: optional version of all above except default(already handled natively).

Serialization

use moneylib::{
    Money, RawMoney,
    iso::{BHD, CHF, EUR, GBP, IDR, JPY, USD},
    macros::{money, raw},
};

// ---------------------------------------------------------------------------
// Mirror of test_deserialize_json — builds the same Order in memory and
// serializes it back to JSON, then asserts every field's serialized form.
//
// Serialization rules per serde module:
//   default            → JSON Number  (e.g. 500.50, -3000, 0)
//   comma_str_code     → "USD 1,234.56"
//   comma_str_symbol   → "$1,234.56"
//   dot_str_code       → "EUR 1.234,56"
//   dot_str_symbol     → "€1.234,56"
//   minor              → integer minor units (e.g. 98000 for ¥98,000)
//   option_*           → null when None, otherwise same as non-option variant
//   str_code           → locale-aware code string  (e.g. "CHF 1'234.57")
//   str_symbol         → locale-aware symbol string (e.g. "₣1'234.56789")
// ---------------------------------------------------------------------------

// ── nested sub-struct using Money<C> (rounded) ─────────────────────────────
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct PaymentDetails {
    #[serde(with = "moneylib::serde::money::comma_str_code")]
    subtotal_usd: Money<USD>,

    #[serde(with = "moneylib::serde::money::comma_str_symbol")]
    shipping_usd: Money<USD>,

    #[serde(with = "moneylib::serde::money::dot_str_code")]
    tax_eur: Money<EUR>,

    #[serde(with = "moneylib::serde::money::dot_str_symbol")]
    discount_eur: Money<EUR>,

    #[serde(with = "moneylib::serde::money::minor")]
    fee_jpy: Money<JPY>,

    #[serde(with = "moneylib::serde::money::comma_str_code")]
    duty_bhd: Money<BHD>,

    #[serde(with = "moneylib::serde::money::option_comma_str_symbol")]
    tip_gbp: Option<Money<GBP>>,

    #[serde(with = "moneylib::serde::money::option_dot_str_code")]
    refund_eur: Option<Money<EUR>>,

    #[serde(with = "moneylib::serde::money::option_minor", default)]
    bonus_jpy: Option<Money<JPY>>,
}

// ── nested sub-struct using RawMoney<C> (full precision) ───────────────────
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct RawLineItems {
    #[serde(with = "moneylib::serde::raw_money::comma_str_code")]
    unit_price_usd: RawMoney<USD>,

    #[serde(with = "moneylib::serde::raw_money::dot_str_symbol")]
    tax_rate_eur: RawMoney<EUR>,

    #[serde(with = "moneylib::serde::raw_money::dot_str_code")]
    spread_bhd: RawMoney<BHD>,

    #[serde(with = "moneylib::serde::raw_money::minor")]
    surcharge_jpy: RawMoney<JPY>,

    #[serde(with = "moneylib::serde::raw_money::option_comma_str_symbol")]
    commission_gbp: Option<RawMoney<GBP>>,

    #[serde(with = "moneylib::serde::raw_money::option_comma_str_code")]
    rebate_usd: Option<RawMoney<USD>>,

    #[serde(with = "moneylib::serde::raw_money::option_dot_str_code", default)]
    adjustment_eur: Option<RawMoney<EUR>>,
}

// ── top-level order struct ──────────────────────────────────────────────────
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct Order {
    order_id: u64,

    // default → JSON Number, rounded to 2 d.p.
    amount_from_f64: Money<USD>,

    // default → JSON Number, integer
    amount_from_i64_neg: Money<EUR>,

    // default → JSON Number
    amount_from_str: Money<USD>,

    #[serde(default)]
    omitted_idr: Money<IDR>,

    #[serde(with = "moneylib::serde::money::option_comma_str_code", default)]
    omitted_opt_usd: Option<Money<USD>>,

    // default → JSON Number, full precision
    raw_amount_from_f64: RawMoney<USD>,

    // default → JSON Number, full precision
    raw_amount_from_str: RawMoney<EUR>,

    #[serde(default)]
    raw_omitted_idr: RawMoney<IDR>,

    #[serde(with = "moneylib::serde::raw_money::option_comma_str_code", default)]
    raw_omitted_opt_usd: Option<RawMoney<USD>>,

    payment: PaymentDetails,
    raw_items: RawLineItems,

    #[serde(with = "moneylib::serde::money::str_code")]
    amount_in_chf_code: Money<CHF>,

    #[serde(with = "moneylib::serde::raw_money::str_symbol")]
    amount_in_chf_symbol: RawMoney<CHF>,
}

// ── build the order in memory ───────────────────────────────────────────────
let order = Order {
    order_id: 98765,

    // Money default → serializes as JSON Number (rounded at creation)
    amount_from_f64: money!(USD, 10000.00), // 9999.9951 rounded → 10000.00
    amount_from_i64_neg: money!(EUR, -3000),
    amount_from_str: money!(USD, 500.50),
    omitted_idr: money!(IDR, 0),
    omitted_opt_usd: None,

    // RawMoney default → serializes as JSON Number (full precision)
    raw_amount_from_f64: raw!(USD, 9999.9951),
    raw_amount_from_str: raw!(EUR, 1234.56789),
    raw_omitted_idr: raw!(IDR, 0),
    raw_omitted_opt_usd: None,

    payment: PaymentDetails {
        // comma_str_code    → "USD 2,500.00"
        subtotal_usd: money!(USD, 2500.00),
        // comma_str_symbol  → "$150.75"
        shipping_usd: money!(USD, 150.75),
        // dot_str_code      → "EUR 1.234,56"
        tax_eur: money!(EUR, 1234.56),
        // dot_str_symbol    → "€500,00"
        discount_eur: money!(EUR, 500.00),
        // minor             → 98000  (¥98,000, 0 d.p.)
        fee_jpy: money!(JPY, 98000),
        // comma_str_code    → "BHD 1,234.567"
        duty_bhd: money!(BHD, 1234.567),
        // option_comma_str_symbol → "£75.00"
        tip_gbp: Some(money!(GBP, 75.00)),
        // option_dot_str_code → null
        refund_eur: None,
        // option_minor, omitted in JSON but present in struct as None
        bonus_jpy: None,
    },

    raw_items: RawLineItems {
        // comma_str_code    → "USD 999.99875"
        unit_price_usd: raw!(USD, 999.99875),
        // dot_str_symbol    → "€1.234,56789"
        tax_rate_eur: raw!(EUR, 1234.56789),
        // dot_str_code      → "BHD 0,12345"
        spread_bhd: raw!(BHD, 0.12345),
        // minor             → 500
        surcharge_jpy: raw!(JPY, 500),
        // option_comma_str_symbol → "£12.34567"
        commission_gbp: Some(raw!(GBP, 12.34567)),
        // option_comma_str_code → null
        rebate_usd: None,
        // option_dot_str_code, omitted → None
        adjustment_eur: None,
    },

    // str_code   → "CHF 1'234.57"  (CHF locale uses apostrophe as thousands separator)
    amount_in_chf_code: money!(CHF, 1234.57),
    // str_symbol → "₣1'234.56789"
    amount_in_chf_symbol: raw!(CHF, 1234.56789),
};

// ── serialize ───────────────────────────────────────────────────────────────
let result = serde_json::to_value(&order);
assert!(result.is_ok(), "serialization failed: {:?}", result.err());
let json = result.unwrap();

// ── top-level Money assertions (default → JSON Number) ─────────────────────

assert_eq!(json["order_id"], 98765);

use serde_json::Number;
use std::str::FromStr;

// Money default → number
assert_eq!(
    json["amount_from_f64"],
    serde_json::Value::Number(Number::from_str("10000.00").unwrap())
);
assert_eq!(json["amount_from_i64_neg"], serde_json::json!(-3000));
assert_eq!(
    json["amount_from_str"],
    serde_json::Value::Number(Number::from_str("500.50").unwrap())
);
assert_eq!(json["omitted_idr"], serde_json::json!(0));

// omitted_opt_usd → None → serializes as null for option_comma_str_code
assert_eq!(json["omitted_opt_usd"], serde_json::Value::Null);

// ── top-level RawMoney assertions (default → JSON Number) ──────────────────

// RawMoney default → number, full precision preserved
assert_eq!(json["raw_amount_from_f64"], serde_json::json!(9999.9951));
assert_eq!(json["raw_amount_from_str"], serde_json::json!(1234.56789));
assert_eq!(json["raw_omitted_idr"], serde_json::json!(0));

// raw_omitted_opt_usd → None → null
assert_eq!(json["raw_omitted_opt_usd"], serde_json::Value::Null);

// ── PaymentDetails (Money, formatted strings) ───────────────────────────────

// comma_str_code: "USD 2,500.00"
assert_eq!(json["payment"]["subtotal_usd"], "USD 2,500.00");

// comma_str_symbol: "$150.75"
assert_eq!(json["payment"]["shipping_usd"], "$150.75");

// dot_str_code: "EUR 1.234,56"
assert_eq!(json["payment"]["tax_eur"], "EUR 1.234,56");

// dot_str_symbol: "€500,00"
assert_eq!(json["payment"]["discount_eur"], "€500,00");

// minor: 98000 (integer, 0-decimal JPY)
assert_eq!(json["payment"]["fee_jpy"], 98000);

// comma_str_code: "BHD 1,234.567"
assert_eq!(json["payment"]["duty_bhd"], "BHD 1,234.567");

// option_comma_str_symbol, Some: "£75.00"
assert_eq!(json["payment"]["tip_gbp"], "£75.00");

// option_dot_str_code, None: null
assert_eq!(json["payment"]["refund_eur"], serde_json::Value::Null);

// option_minor, None: null
assert_eq!(json["payment"]["bonus_jpy"], serde_json::Value::Null);

// ── RawLineItems (RawMoney, formatted strings) ──────────────────────────────

// comma_str_code: "USD 999.99875"  (no thousands separator needed for 3-digit integer part)
assert_eq!(json["raw_items"]["unit_price_usd"], "USD 999.99875");

// dot_str_symbol: "€1.234,56789"
assert_eq!(json["raw_items"]["tax_rate_eur"], "€1.234,56789");

// dot_str_code: "BHD 0,12345"  (0 integer part, comma as decimal separator)
assert_eq!(json["raw_items"]["spread_bhd"], "BHD 0,12345");

// minor: 500 (integer, 0-decimal JPY)
assert_eq!(json["raw_items"]["surcharge_jpy"], 500);

// option_comma_str_symbol, Some: "£12.34567"
assert_eq!(json["raw_items"]["commission_gbp"], "£12.34567");

// option_comma_str_code, None: null
assert_eq!(json["raw_items"]["rebate_usd"], serde_json::Value::Null);

// option_dot_str_code, None: null
assert_eq!(json["raw_items"]["adjustment_eur"], serde_json::Value::Null);

// ── CHF locale-aware str_code / str_symbol ──────────────────────────────────

// str_code: CHF uses apostrophe as thousands separator → "CHF 1'234.57"
assert_eq!(json["amount_in_chf_code"], "CHF 1'234.57");

// str_symbol: "₣1'234.56789"
assert_eq!(json["amount_in_chf_symbol"], "₣1'234.56789");

// ── round-trip sanity: deserialize back and compare key fields ──────────────
let round_tripped: Order = serde_json::from_value(json).unwrap();

assert_eq!(round_tripped.order_id, order.order_id);
assert_eq!(round_tripped.amount_from_f64, order.amount_from_f64);
assert_eq!(round_tripped.amount_from_i64_neg, order.amount_from_i64_neg);
assert_eq!(round_tripped.amount_from_str, order.amount_from_str);
assert_eq!(round_tripped.omitted_idr, order.omitted_idr);
assert!(round_tripped.omitted_opt_usd.is_none());
assert_eq!(round_tripped.raw_amount_from_f64, order.raw_amount_from_f64);
assert_eq!(round_tripped.raw_amount_from_str, order.raw_amount_from_str);
assert_eq!(round_tripped.raw_omitted_idr, order.raw_omitted_idr);
assert!(round_tripped.raw_omitted_opt_usd.is_none());
assert_eq!(
    round_tripped.payment.subtotal_usd,
    order.payment.subtotal_usd
);
assert_eq!(
    round_tripped.payment.shipping_usd,
    order.payment.shipping_usd
);
assert_eq!(round_tripped.payment.tax_eur, order.payment.tax_eur);
assert_eq!(
    round_tripped.payment.discount_eur,
    order.payment.discount_eur
);
assert_eq!(round_tripped.payment.fee_jpy, order.payment.fee_jpy);
assert_eq!(round_tripped.payment.duty_bhd, order.payment.duty_bhd);
assert_eq!(round_tripped.payment.tip_gbp, order.payment.tip_gbp);
assert!(round_tripped.payment.refund_eur.is_none());
assert!(round_tripped.payment.bonus_jpy.is_none());
assert_eq!(
    round_tripped.raw_items.unit_price_usd,
    order.raw_items.unit_price_usd
);
assert_eq!(
    round_tripped.raw_items.tax_rate_eur,
    order.raw_items.tax_rate_eur
);
assert_eq!(
    round_tripped.raw_items.spread_bhd,
    order.raw_items.spread_bhd
);
assert_eq!(
    round_tripped.raw_items.surcharge_jpy,
    order.raw_items.surcharge_jpy
);
assert_eq!(
    round_tripped.raw_items.commission_gbp,
    order.raw_items.commission_gbp
);
assert!(round_tripped.raw_items.rebate_usd.is_none());
assert!(round_tripped.raw_items.adjustment_eur.is_none());
assert_eq!(round_tripped.amount_in_chf_code, order.amount_in_chf_code);
assert_eq!(
    round_tripped.amount_in_chf_symbol,
    order.amount_in_chf_symbol
);

Deserialization

use moneylib::{
    BaseMoney, Money, RawMoney,
    iso::{BHD, CHF, EUR, GBP, IDR, JPY, USD},
    macros::{dec, money, raw},
};

// ---------------------------------------------------------------------------
// Simulates deserializing a realistic multi-currency JSON request payload
// with BOTH Money<C> (auto-rounded) and RawMoney<C> (full precision).
//
// Currencies by minor unit:
//   0 decimal places : JPY
//   2 decimal places : USD, EUR, GBP
//   3 decimal places : BHD
//
// Parsing modes exercised (both Money and RawMoney namespaces):
//   - default          (number / string-number)
//   - comma_str_code   "USD 1,234.56"
//   - comma_str_symbol "$1,234.56"
//   - dot_str_code     "EUR 1.234,56"
//   - dot_str_symbol   "€1.234,56"
//   - minor            integer minor-unit
//   - str_code           → locale-aware code string  (e.g. "CHF 1'234.57")
//   - str_symbol         → locale-aware symbol string (e.g. "₣1'234.56789")
//   - option_*         nullable counterparts of the above
//   - #[serde(default)] field omitted → zero / None
//
// Key difference between Money and RawMoney:
//   Money  → always rounded to currency's minor unit on creation
//   RawMoney → preserves full decimal precision; call .finish() to round
//
// Nesting: payment details (Money) and raw line items (RawMoney) are
// separate nested sub-structs inside the top-level Order.
// ---------------------------------------------------------------------------

// ── nested sub-struct using Money<C> (rounded) ─────────────────────────────
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct PaymentDetails {
    // 2-decimal, comma+code: "USD 2,500.00"
    #[serde(with = "moneylib::serde::money::comma_str_code")]
    subtotal_usd: Money<USD>,

    // 2-decimal, comma+symbol: "$150.75"
    #[serde(with = "moneylib::serde::money::comma_str_symbol")]
    shipping_usd: Money<USD>,

    // 2-decimal, dot+code: "EUR 1.234,56"
    #[serde(with = "moneylib::serde::money::dot_str_code")]
    tax_eur: Money<EUR>,

    // 2-decimal, dot+symbol: "€500,00"
    #[serde(with = "moneylib::serde::money::dot_str_symbol")]
    discount_eur: Money<EUR>,

    // 0-decimal, minor integer: 98000 → ¥98,000
    #[serde(with = "moneylib::serde::money::minor")]
    fee_jpy: Money<JPY>,

    // 3-decimal, comma+code: "BHD 1,234.567"
    #[serde(with = "moneylib::serde::money::comma_str_code")]
    duty_bhd: Money<BHD>,

    // 2-decimal optional GBP, present: "£75.00"
    #[serde(with = "moneylib::serde::money::option_comma_str_symbol")]
    tip_gbp: Option<Money<GBP>>,

    // 2-decimal optional EUR, explicit null
    #[serde(with = "moneylib::serde::money::option_dot_str_code")]
    refund_eur: Option<Money<EUR>>,

    // 0-decimal optional JPY, omitted → None via default
    #[serde(with = "moneylib::serde::money::option_minor", default)]
    bonus_jpy: Option<Money<JPY>>,
}

// ── nested sub-struct using RawMoney<C> (full precision, no rounding) ──────
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct RawLineItems {
    // Full-precision USD unit price — comma+code, preserves all decimals
    #[serde(with = "moneylib::serde::raw_money::comma_str_code")]
    unit_price_usd: RawMoney<USD>,

    // Full-precision EUR tax rate — dot+symbol, no rounding
    #[serde(with = "moneylib::serde::raw_money::dot_str_symbol")]
    tax_rate_eur: RawMoney<EUR>,

    // Full-precision BHD exchange spread — dot+code, 3-decimal but more precision
    #[serde(with = "moneylib::serde::raw_money::dot_str_code")]
    spread_bhd: RawMoney<BHD>,

    // Full-precision JPY surcharge — minor integer (0-decimal)
    #[serde(with = "moneylib::serde::raw_money::minor")]
    surcharge_jpy: RawMoney<JPY>,

    // Optional GBP commission, comma+symbol, present
    #[serde(with = "moneylib::serde::raw_money::option_comma_str_symbol")]
    commission_gbp: Option<RawMoney<GBP>>,

    // Optional USD rebate, comma+code, explicit null
    #[serde(with = "moneylib::serde::raw_money::option_comma_str_code")]
    rebate_usd: Option<RawMoney<USD>>,

    // Optional EUR adjustment, dot+code, omitted → None via default
    #[serde(with = "moneylib::serde::raw_money::option_dot_str_code", default)]
    adjustment_eur: Option<RawMoney<EUR>>,
}

// ── top-level order struct ──────────────────────────────────────────────────
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct Order {
    order_id: u64,

    // Money: default number (f64 path) — will be rounded to 2 d.p.
    amount_from_f64: Money<USD>,

    // Money: default number (i64 path, negative)
    amount_from_i64_neg: Money<EUR>,

    // Money: default string-number "500.50"
    amount_from_str: Money<USD>,

    // Money: omitted field → zero IDR
    #[serde(default)]
    omitted_idr: Money<IDR>,

    // Money: optional omitted → None
    #[serde(with = "moneylib::serde::money::option_comma_str_code", default)]
    omitted_opt_usd: Option<Money<USD>>,

    // RawMoney: default number (f64 path) — preserves full precision, no rounding
    raw_amount_from_f64: RawMoney<USD>,

    // RawMoney: default string-number — preserves all fractional digits
    raw_amount_from_str: RawMoney<EUR>,

    // RawMoney: omitted field → zero IDR
    #[serde(default)]
    raw_omitted_idr: RawMoney<IDR>,

    // RawMoney: optional omitted → None
    #[serde(with = "moneylib::serde::raw_money::option_comma_str_code", default)]
    raw_omitted_opt_usd: Option<RawMoney<USD>>,

    // nested rounded payment breakdown
    payment: PaymentDetails,

    // nested full-precision line items
    raw_items: RawLineItems,

    // str code: CHF
    #[serde(with = "moneylib::serde::money::str_code")]
    amount_in_chf_code: Money<CHF>,

    // str symbol: CHF
    #[serde(with = "moneylib::serde::raw_money::str_symbol")]
    amount_in_chf_symbol: RawMoney<CHF>,
}

// ── JSON input ──────────────────────────────────────────────────────────────
let json_str = r#"
    {
      "order_id": 98765,

      "amount_from_f64": 9999.9951,
      "amount_from_i64_neg": -3000,
      "amount_from_str": "500.50",

      "raw_amount_from_f64": 9999.9951,
      "raw_amount_from_str": "1234.56789",

      "payment": {
        "subtotal_usd": "USD 2,500.00",
        "shipping_usd": "$150.75",
        "tax_eur":      "EUR 1.234,56",
        "discount_eur": "€500,00",
        "fee_jpy":      98000,
        "duty_bhd":     "BHD 1,234.567",
        "tip_gbp":      "£75.00",
        "refund_eur":   null
      },

      "raw_items": {
        "unit_price_usd":  "USD 999.99875",
        "tax_rate_eur":    "€1.234,56789",
        "spread_bhd":      "BHD 0,12345",
        "surcharge_jpy":   500,
        "commission_gbp":  "£12.34567",
        "rebate_usd":      null
      },

      "amount_in_chf_code": "CHF 1'234.56789",
      "amount_in_chf_symbol": "₣1'234.56789"
    }
    "#;

// ── parse ───────────────────────────────────────────────────────────────────
let order = serde_json::from_str::<Order>(json_str);
assert!(order.is_ok(), "deserialization failed: {:?}", order.err());
let order = order.unwrap();

// ── Money top-level assertions ──────────────────────────────────────────────

assert_eq!(order.order_id, 98765);

// 9999.9951 → rounded to 2 d.p. (bankers) → 10000.00
assert_eq!(order.amount_from_f64.amount(), dec!(10000.00));
assert_eq!(order.amount_from_f64.code(), "USD");

// -3000 i64 → EUR, no fractional part
assert_eq!(order.amount_from_i64_neg.amount(), dec!(-3000));
assert_eq!(order.amount_from_i64_neg.code(), "EUR");

// "500.50" string-number → USD, exact
assert_eq!(order.amount_from_str.amount(), dec!(500.50));
assert_eq!(order.amount_from_str.code(), "USD");

// omitted Money field → zero IDR
assert_eq!(order.omitted_idr.amount(), dec!(0));
assert_eq!(order.omitted_idr.code(), "IDR");

// omitted optional Money → None
assert!(order.omitted_opt_usd.is_none());

// ── RawMoney top-level assertions ───────────────────────────────────────────

// 9999.9951 → RawMoney keeps full precision, no rounding
assert_eq!(order.raw_amount_from_f64.amount(), dec!(9999.9951));
assert_eq!(order.raw_amount_from_f64.code(), "USD");

// "1234.56789" → RawMoney preserves all 5 decimal places
assert_eq!(order.raw_amount_from_str.amount(), dec!(1234.56789));
assert_eq!(order.raw_amount_from_str.code(), "EUR");

// omitted RawMoney field → zero IDR
assert_eq!(order.raw_omitted_idr.amount(), dec!(0));
assert_eq!(order.raw_omitted_idr.code(), "IDR");

// omitted optional RawMoney → None
assert!(order.raw_omitted_opt_usd.is_none());

// ── Money nested PaymentDetails assertions ──────────────────────────────────

// subtotal_usd — comma+code, 2-decimal USD, rounded
assert_eq!(order.payment.subtotal_usd.amount(), dec!(2500.00));
assert_eq!(order.payment.subtotal_usd.code(), "USD");

// shipping_usd — comma+symbol, 2-decimal USD
assert_eq!(order.payment.shipping_usd.amount(), dec!(150.75));
assert_eq!(order.payment.shipping_usd.code(), "USD");

// tax_eur — dot+code, European format EUR
assert_eq!(order.payment.tax_eur.amount(), dec!(1234.56));
assert_eq!(order.payment.tax_eur.code(), "EUR");

// discount_eur — dot+symbol, "€500,00" → 500.00
assert_eq!(order.payment.discount_eur.amount(), dec!(500.00));
assert_eq!(order.payment.discount_eur.code(), "EUR");

// fee_jpy — minor integer, 0-decimal JPY: 98000 → ¥98,000
assert_eq!(order.payment.fee_jpy.amount(), dec!(98000));
assert_eq!(order.payment.fee_jpy.code(), "JPY");
assert_eq!(order.payment.fee_jpy.minor_unit(), 0);

// duty_bhd — comma+code, 3-decimal BHD
assert_eq!(order.payment.duty_bhd.amount(), dec!(1234.567));
assert_eq!(order.payment.duty_bhd.code(), "BHD");
assert_eq!(order.payment.duty_bhd.minor_unit(), 3);

// tip_gbp — option_comma_str_symbol GBP, present
assert!(order.payment.tip_gbp.is_some());
assert_eq!(
    order.payment.tip_gbp.as_ref().unwrap().amount(),
    dec!(75.00)
);
assert_eq!(order.payment.tip_gbp.as_ref().unwrap().code(), "GBP");

// refund_eur — option_dot_str_code EUR, explicit null
assert!(order.payment.refund_eur.is_none());

// bonus_jpy — option_minor JPY, omitted → None
assert!(order.payment.bonus_jpy.is_none());

// ── RawMoney nested RawLineItems assertions ─────────────────────────────────

// unit_price_usd — comma+code, full precision USD (no rounding)
assert_eq!(order.raw_items.unit_price_usd.amount(), dec!(999.99875));
assert_eq!(order.raw_items.unit_price_usd.code(), "USD");
// verify .finish() would round to 2 d.p.
assert_eq!(
    order.raw_items.unit_price_usd.finish().amount(),
    dec!(1000.00)
);

// tax_rate_eur — dot+symbol, full 5 d.p. EUR preserved
assert_eq!(order.raw_items.tax_rate_eur.amount(), dec!(1234.56789));
assert_eq!(order.raw_items.tax_rate_eur.code(), "EUR");
// .finish() rounds to 2 d.p.
assert_eq!(
    order.raw_items.tax_rate_eur.finish().amount(),
    dec!(1234.57)
);

// spread_bhd — dot+code BHD, 5 d.p. (more than BHD's 3 minor unit)
assert_eq!(order.raw_items.spread_bhd.amount(), dec!(0.12345));
assert_eq!(order.raw_items.spread_bhd.code(), "BHD");
// .finish() rounds to 3 d.p. (BHD minor unit)
assert_eq!(order.raw_items.spread_bhd.finish().amount(), dec!(0.123));

// surcharge_jpy — minor integer, 0-decimal JPY: 500 → ¥500
assert_eq!(order.raw_items.surcharge_jpy.amount(), dec!(500));
assert_eq!(order.raw_items.surcharge_jpy.code(), "JPY");
assert_eq!(order.raw_items.surcharge_jpy.minor_unit(), 0);

// commission_gbp — option_comma_str_symbol GBP, full precision, present
assert!(order.raw_items.commission_gbp.is_some());
assert_eq!(
    order.raw_items.commission_gbp.as_ref().unwrap().amount(),
    dec!(12.34567)
);
assert_eq!(
    order.raw_items.commission_gbp.as_ref().unwrap().code(),
    "GBP"
);
// .finish() rounds to 2 d.p.
assert_eq!(
    order.raw_items.commission_gbp.unwrap().finish().amount(),
    dec!(12.35)
);

// rebate_usd — option_comma_str_code USD, explicit null
assert!(order.raw_items.rebate_usd.is_none());

// adjustment_eur — option_dot_str_code EUR, omitted → None
assert!(order.raw_items.adjustment_eur.is_none());

assert_eq!(order.amount_in_chf_code, money!(CHF, 1_234.57));
assert_eq!(order.amount_in_chf_symbol, raw!(CHF, 1_234.56789));

Accounting

Requires feature accounting

Some accounting operations. Currently support:

  • Interest calculations.

Interest

Interest calculations supported:

  • Fixed and compounding interests.
  • Calculating yield.
  • Calculating future value.
  • Calculating present value.
  • Calculating PMT.

It's based on trait InterestOps<C> implemented by types implementing BaseMoney<C>(Money and RawMoney). This trait has 2 methods that start the builder process ending with calculation.

2 methods starting the builder of interest params:

  • interest_fixed: calculate FV, PV, and PMT.
  • interest_compound: calculate FV and PV.

These methods return struct Interest which provides builder pattern interface for interest calculation params like:

  • Principal amount
  • Rate percentage as daily, monthly, or yearly/annually.
  • Total period in days, months, years, quarters, semi-annuals.
  • Number of days in accounting calculation according to the standard, e.g. 30/360, 30/365, Actual/Actual, etc.
  • year: starting year of calculation.
  • month: starting month of calculation.
  • day: starting day of calculation.
  • contribs: contributions each time of period, max is total period minus 1, the contribution start at the second unit of period.
  • tax: flat rate tax applied to each period.

After these params set(if ignored, default will be set), you can call these methods:

  • returns: calculate total interests returned.
  • future_value: calculate FV.
  • present_value: calculate PV.
  • payment: calculate PMT.
use moneylib::{
    BaseMoney,
    accounting::interest::{InterestOps, RateDays},
    macros::{dec, money},
};

// -------------------------------------------------------------------------
// Scenario: a small investment platform processes four customer accounts.
//
// Account A  – USD savings, fixed-rate, yearly periods, with tax.
// Account B  – EUR mortgage, fixed-rate, monthly periods → PMT.
// Account C  – JPY (0-decimal) compounding daily, with contributions.
// Account D  – BHD (3-decimal) compounding, quarterly + semi-annual periods.
//
// Every account is tested for ALL four outputs:
//   .returns() / .future_value() / .present_value() / .payment()
//
// Builder features exercised:
//   .daily() / .monthly() / .yearly()
//   .days() / .months() / .years() / .quarters() / .semi_annuals()
//   .year() / .month() / .day()
//   .rate_days(RateDays::*)
//   .with_contribs()
//   .with_tax()
//   interest_fixed() / interest_compound()
// -------------------------------------------------------------------------

// =========================================================================
// Account A: USD – fixed yearly rate, yearly periods, with tax
// =========================================================================
//
// P = $5,000 | r = 5% yearly | 3 years | RateActualActual (default)
//
// returns():
//   fixed: returns = P × (r/100) × t = 5000 × 0.05 × 3 = 750
//
// future_value():
//   FV = P + returns = 5750
//   After 20% flat-rate tax on returns: net_returns = 750 × 0.80 = 600 → FV = 5600
//
// present_value() round-trip:
//   PV formula (fixed): PV = FV / (1 + r×t) = 5750 / (1 + 0.15) = 5000
//
// payment() (PMT):
//   PMT = P × r × (1+r)^n / [(1+r)^n − 1]
//   r = 0.05 (yearly), n = 3 years → PMT ≈ 1835.46 per year

let account_a = money!(USD, 5000);

let inv_a = account_a
    .interest_fixed(5)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .years(3);

// .returns()
assert_eq!(inv_a.returns().unwrap().amount(), dec!(750.00));
assert_eq!(inv_a.returns().unwrap().code(), "USD");

// .future_value()
assert_eq!(inv_a.future_value().unwrap().amount(), dec!(5750.00));

// .future_value() after 20% tax
assert_eq!(
    inv_a.with_tax(20).unwrap().future_value().unwrap().amount(),
    dec!(5600.00)
);

// .present_value() round-trip
let fv_a = inv_a.future_value().unwrap();
let pv_a = fv_a
    .interest_fixed(5)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .years(3)
    .present_value()
    .unwrap();
assert_eq!(pv_a, account_a);

// .payment()
// r=0.05 yearly, n=3: PMT = 5000 × 0.05 × (1.05)^3 / [(1.05)^3 − 1]
let pmt_a = inv_a.payment().unwrap();
assert_eq!(pmt_a.amount(), dec!(1836.04));
assert_eq!(pmt_a.code(), "USD");

// =========================================================================
// Account B: EUR – fixed yearly rate, monthly periods → PMT (mortgage)
// =========================================================================
//
// P = €200,000 | r = 4.8% yearly | Rate30360
//
// returns() + future_value() on a 2-month slice:
//   monthly_rate = 4.8/12/100 = 0.004
//   returns = 200000 × 0.004 × 2 = 1600
//   FV = 201600
//
// present_value() round-trip on the same 2-month slice.
//
// payment() on full 360-month (30-year) mortgage:
//   PMT = P × r × (1+r)^360 / [(1+r)^360 − 1] ≈ €1,048.82

let account_b = money!(EUR, 200000);

// 2-month slice for returns / FV / PV
let inv_b_short = account_b
    .interest_fixed(dec!(4.8))
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .rate_days(RateDays::Rate30360)
    .months(2);

// .returns()
assert_eq!(inv_b_short.returns().unwrap().amount(), dec!(1600.00));
assert_eq!(inv_b_short.returns().unwrap().code(), "EUR");

// .future_value()
assert_eq!(
    inv_b_short.future_value().unwrap().amount(),
    dec!(201600.00)
);

// .present_value() round-trip
let fv_b = inv_b_short.future_value().unwrap();
let pv_b = fv_b
    .interest_fixed(dec!(4.8))
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .rate_days(RateDays::Rate30360)
    .months(2)
    .present_value()
    .unwrap();
assert_eq!(pv_b, account_b);

// .payment() on full 30-year mortgage
let pmt_b = account_b
    .interest_fixed(dec!(4.8))
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .rate_days(RateDays::Rate30360)
    .months(360)
    .payment()
    .unwrap();
assert_eq!(pmt_b.amount(), dec!(1049.33));
assert_eq!(pmt_b.code(), "EUR");

// =========================================================================
// Account C: JPY (0-decimal) – compounding daily, with contributions + tax
// =========================================================================
//
// P = ¥500,000 | r = 1% daily | 3 days | contribs = [+¥50,000, -¥20,000]
//
// returns():
//   Day 1: 500000×0.01=5000, balance=505000, +50000 → 555000
//   Day 2: 555000×0.01=5550, balance=560550, -20000 → 540550
//   Day 3: 540550×0.01=5406 (JPY rounds to 0dp)
//   total returns = 5000 + 5550 + 5406 = 15956
//
// future_value():
//   FV = P + contribs_sum + returns = 500000 + 30000 + 15956 = 545956
//   After 10% tax: net_returns = 15956 × 0.90 = 14360 → FV = 544360
//
// present_value():
//   No contrib/tax for a clean PV round-trip: use a simple 2-day no-contrib slice.
//   r=1% daily, 2 days: divisor = (1.01)^2 = 1.0201
//   FV = 500000 × (1.01)^2 = 510050 → PV = 510050 / 1.0201 = 500000
//
// payment():
//   r=1% daily, 5 days:
//   PMT = 500000 × r × (1+r)^5 / [(1+r)^5 − 1] ≈ ¥102,030

let account_c = money!(JPY, 500000);
let contribs_c = [money!(JPY, 50000), money!(JPY, -20000)];

let inv_c = account_c
    .interest_compound(1)
    .unwrap()
    .daily()
    .year(2026)
    .month(1)
    .day(1)
    .days(3)
    .with_contribs(&contribs_c)
    .unwrap();

// .returns()
assert_eq!(inv_c.returns().unwrap().amount(), dec!(15956));
assert_eq!(inv_c.returns().unwrap().code(), "JPY");

// .future_value()
assert_eq!(inv_c.future_value().unwrap().amount(), dec!(545956));

// .future_value() after 10% tax
assert_eq!(
    inv_c.with_tax(10).unwrap().future_value().unwrap().amount(),
    dec!(544360)
);

// .present_value() round-trip (clean 2-day compounding, no contribs)
let inv_c_clean = account_c
    .interest_compound(1)
    .unwrap()
    .daily()
    .year(2026)
    .month(1)
    .day(1)
    .days(2);
let fv_c = inv_c_clean.future_value().unwrap();
let pv_c = fv_c
    .interest_compound(1)
    .unwrap()
    .daily()
    .year(2026)
    .month(1)
    .day(1)
    .days(2)
    .present_value()
    .unwrap();
assert_eq!(pv_c, account_c);

// .payment()
// PMT = 500000 × 0.01 × (1.01)^5 / [(1.01)^5 − 1]
let pmt_c = account_c
    .interest_fixed(1)
    .unwrap()
    .daily()
    .year(2026)
    .month(1)
    .day(1)
    .days(5)
    .payment()
    .unwrap();
assert_eq!(pmt_c.amount(), dec!(103020));
assert_eq!(pmt_c.code(), "JPY");

// minor_unit check: JPY has 0 decimal places
assert_eq!(account_c.minor_unit(), 0);

// =========================================================================
// Account D: BHD (3-decimal) – compounding, quarterly + semi-annual
// =========================================================================
//
// --- D-Quarterly ---
// P = BHD 10,000 | r = 8% yearly | 4 quarters | Rate30360
// quarterly_rate = 8/4/100 = 0.02
//
// returns():
//   Q1: 10000×0.02=200,      balance=10200
//   Q2: 10200×0.02=204,      balance=10404
//   Q3: 10404×0.02=208.08,   balance=10612.08
//   Q4: 10612.08×0.02=212.2416, balance=10824.3216 → BHD rounds to 10824.322
//   returns = 824.322
//
// future_value() = 10824.322
//
// present_value() round-trip.
//
// payment():
//   PMT = 10000 × 0.02 × (1.02)^4 / [(1.02)^4 − 1] ≈ BHD 2,626.238

let account_d = money!(BHD, 10000);

let inv_d_quarterly = account_d
    .interest_compound(8)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .rate_days(RateDays::Rate30360)
    .quarters(4);

// .returns()
assert_eq!(inv_d_quarterly.returns().unwrap().amount(), dec!(824.322));
assert_eq!(inv_d_quarterly.returns().unwrap().code(), "BHD");

// .future_value()
assert_eq!(
    inv_d_quarterly.future_value().unwrap().amount(),
    dec!(10824.322)
);

// .present_value() round-trip
let fv_d_q = inv_d_quarterly.future_value().unwrap();
let pv_d_q = fv_d_q
    .interest_compound(8)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .rate_days(RateDays::Rate30360)
    .quarters(4)
    .present_value()
    .unwrap();
assert_eq!(pv_d_q, account_d);

// .payment()
// PMT = 10000 × 0.02 × (1.02)^4 / [(1.02)^4 − 1]
let pmt_d_q = account_d
    .interest_fixed(8)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .rate_days(RateDays::Rate30360)
    .quarters(4)
    .payment()
    .unwrap();
assert_eq!(pmt_d_q.amount(), dec!(2626.238));
assert_eq!(pmt_d_q.code(), "BHD");

// --- D-SemiAnnual ---
// P = BHD 10,000 | r = 8% yearly | 2 semi-annuals | Rate30360
// semi_annual_rate = 8/2/100 = 0.04
//
// returns():
//   S1: 10000×0.04=400, balance=10400
//   S2: 10400×0.04=416, balance=10816
//   returns = 816
//
// future_value() = 10816
//
// present_value() round-trip.
//
// payment():
//   PMT = 10000 × 0.04 × (1.04)^2 / [(1.04)^2 − 1] ≈ BHD 5,301.961

let inv_d_semi = account_d
    .interest_compound(8)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .rate_days(RateDays::Rate30360)
    .semi_annuals(2);

// .returns()
assert_eq!(inv_d_semi.returns().unwrap().amount(), dec!(816.000));
assert_eq!(inv_d_semi.returns().unwrap().code(), "BHD");

// .future_value()
assert_eq!(inv_d_semi.future_value().unwrap().amount(), dec!(10816.000));

// .present_value() round-trip
let fv_d_s = inv_d_semi.future_value().unwrap();
let pv_d_s = fv_d_s
    .interest_compound(8)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .rate_days(RateDays::Rate30360)
    .semi_annuals(2)
    .present_value()
    .unwrap();
assert_eq!(pv_d_s, account_d);

// .payment()
// PMT = 10000 × 0.04 × (1.04)^2 / [(1.04)^2 − 1]
let pmt_d_s = account_d
    .interest_fixed(8)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .rate_days(RateDays::Rate30360)
    .semi_annuals(2)
    .payment()
    .unwrap();
assert_eq!(pmt_d_s.amount(), dec!(5301.961));
assert_eq!(pmt_d_s.code(), "BHD");

// minor_unit check: BHD has 3 decimal places
assert_eq!(account_d.minor_unit(), 3);

// quarterly compound always beats semi-annual (more compounding periods)
assert!(
    inv_d_quarterly.returns().unwrap().amount() > inv_d_semi.returns().unwrap().amount()
);

// =========================================================================
// Cross-cutting: compound always beats fixed for the same inputs
// =========================================================================

let principal = money!(USD, 10000);

let fixed_returns = principal
    .interest_fixed(6)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .years(5)
    .returns()
    .unwrap();

let compound_returns = principal
    .interest_compound(6)
    .unwrap()
    .yearly()
    .year(2026)
    .month(1)
    .day(1)
    .years(5)
    .returns()
    .unwrap();

assert!(compound_returns.amount() > fixed_returns.amount());