moneylib
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 ;
let money = new.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!; // truncated into 123, and 123 == 123.0
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!
RawMoney<C>
use ;
let money = new.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!; // truncated into 123, and 123 == 123.0
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!
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:
Example of ISO 4217 Currency from the list:
use 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:
;
...and use it:
use ;
let btc = money!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
let btc_raw = raw!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
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>
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 fromRequires feature
raw_money
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 combiningBaseOps<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.RoundingStrategyenum: rounding strategy for custom rounding.MoneyErrorenum: error type.- Helper macros:
money!: create hardcodedMoney<C>type.raw!: create hardcodedRawMoney<C>type.dec!: create hardcodedDecimaltype.
Exchangetrait: trait for currencies conversion.ExchangeRatesstruct: contain rates for conversions.isomodule: contain all ISO 4217 currencies to use whereCurrencytrait is expected.accountingmodule: module containing all accounting operations.serdemodule: module containing all serde implementations.
...or you can just import all of them via prelue:
use *;
...
Constructors
You can construct types via concrete types: Money<C> and RawMoney<C>.
Money<C>
use ;
use FromStr;
// using BaseMoney::new
let money = new;
let money2 = new;
assert!;
assert!;
assert_eq!;
let money = new.unwrap;
let money2 = new.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let money = new.unwrap;
let money2 = new.unwrap;
assert_eq!;
assert_eq!;
assert_ne!;
let overflow_money = new;
assert!;
// using from_decimal
use ;
let money = from_decimal; // USD 10,000.00
let money2 = from_decimal; // USD 10,000.00
assert_eq!;
// using from_minor
let money = from_minor.unwrap; // USD 13,402.01
let money2 = from_decimal; // USD 13,402.01
assert_eq!;
// constructing from string with codes and symbols format
// from string amount
let money: = from_str.unwrap;
assert_eq!;
// from string with code, comma thousands separator and dot decimal separator.
// using comma for thousands separator is optional.
let money: = from_code_comma_thousands.unwrap;
assert_eq!;
let money: = from_code_comma_thousands.unwrap;
assert_eq!;
let money: = from_code_comma_thousands.unwrap;
assert_eq!;
// from string with code, dot thousands separator and comma decimal separator.
// using dot for thousands separator is optional.
let money: = from_code_dot_thousands.unwrap;
assert_eq!;
let money: = from_code_dot_thousands.unwrap;
assert_eq!;
let money: = from_code_dot_thousands.unwrap;
assert_eq!;
// from string with symbol, comma thousands separator and dot decimal separator.
// using comma for thousands separator is optional.
let money: = from_symbol_comma_thousands.unwrap;
assert_eq!;
let money: = from_symbol_comma_thousands.unwrap;
assert_eq!;
let money: = from_symbol_comma_thousands.unwrap;
assert_eq!;
// from string with code, dot thousands separator and comma decimal separator.
// using dot for thousands separator is optional.
let money: = from_symbol_dot_thousands.unwrap;
assert_eq!;
let money: = from_symbol_dot_thousands.unwrap;
assert_eq!;
let money: = from_symbol_dot_thousands.unwrap;
assert_eq!;
// parsing string with code using locale separators
let money: = from_code_locale_separator.unwrap;
assert_eq!;
// parsing string with symbol using locale separators
let money: = from_symbol_locale_separator.unwrap;
assert_eq!;
RawMoney<C>
Requires feature
raw_money
use ;
use ;
use FromStr;
// using BaseMoney::new
let money = new.unwrap;
let money2 = new.unwrap;
assert_ne!;
assert_eq!;
assert_eq!;
let money = new.unwrap;
let money2 = new.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let overflow_money = new;
assert!;
// using from_decimal
let money = from_decimal; // USD 10,000.00
let money2 = from_decimal; // USD 10,000.002
assert_ne!;
// using from_minor
let money = from_minor.unwrap; // USD 13,402.01
let money2 = from_decimal; // USD 13,402.01
assert_eq!;
// constructing from string with codes and symbols format
// from string amount
let money: = from_str.unwrap;
assert_eq!;
// from string with code, comma thousands separator and dot decimal separator.
// using comma for thousands separator is optional.
let money: = from_code_comma_thousands.unwrap;
assert_eq!;
let money: = from_code_comma_thousands.unwrap;
assert_eq!;
let money: = from_code_comma_thousands.unwrap;
assert_eq!;
// from string with code, dot thousands separator and comma decimal separator.
// using dot for thousands separator is optional.
let money: = from_code_dot_thousands.unwrap;
assert_eq!;
let money: = from_code_dot_thousands.unwrap;
assert_eq!;
let money: = from_code_dot_thousands.unwrap;
assert_eq!;
// from string with symbol, comma thousands separator and dot decimal separator.
// using comma for thousands separator is optional.
let money: = from_symbol_comma_thousands.unwrap;
assert_eq!;
let money: = from_symbol_comma_thousands.unwrap;
assert_eq!;
let money: = from_symbol_comma_thousands.unwrap;
assert_eq!;
// from string with code, dot thousands separator and comma decimal separator.
// using dot for thousands separator is optional.
let money: = from_symbol_dot_thousands.unwrap;
assert_eq!;
let money: = from_symbol_dot_thousands.unwrap;
assert_eq!;
let money: = from_symbol_dot_thousands.unwrap;
assert_eq!;
// parsing string with code using locale separators
let money: =
from_code_locale_separator.unwrap;
assert_eq!;
// parsing string with symbol using locale separators
let money: =
from_symbol_locale_separator.unwrap;
assert_eq!;
Decimal
You can construct decimal amount using re-exported Decimal type or macro dec!.
use ;
use FromStr;
let hardcoded_dec = dec!; // any rust number format works
let from_string = from_str.unwrap;
let from = from; // only integers
let try_from = try_from.unwrap;
let try_from_i128 = try_from.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_ne!;
assert_ne!;
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 ;
// Money<C> and iso currencies don't need to be in scope.
let money = money!;
assert_eq!;
raw!
Requires feature
raw_money
use ;
// RawMoney<C> and iso currencies don't need to be in scope.
let money = raw!;
assert_eq!;
RawMoney<C> conversion
One good usecase forRequires feature
raw_money
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 ;
// required `raw_money` feature enabled.
let money = money!;
let raw_money = money.into_raw;
assert_eq!;
assert_eq!;
assert_eq!;
// .. do some calculations with raw_money,
let after_calc = raw_money.checked_mul.unwrap;
assert_eq!;
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!;
let raw = raw!;
let trunc = raw.truncate_with;
assert_eq!;
let round = trunc.round;
assert_eq!;
let round_1_scale = round.round_with;
assert_eq!;
// convert to tender money
let money = round_1_scale.finish;
assert_eq!;
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.
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 ;
let neg_money = money!;
let neg_money2 = -money!;
assert_eq!;
Requires feature
raw_money
use ;
let neg_money = raw!;
let neg_money2 = -raw!;
assert_eq!;
Comparison
Comparisons can be done through comparison operators: ==, <, >, <=, >= and BaseOps<C> is_approx method:
Operator Overloading
use money;
use ;
let money = from_decimal;
let money2 = money!;
let same = money == money2;
assert!;
let money3 = money!;
// let same = money == money3; // compile error
let money4 = money!;
let bigger = money3 > money4;
assert!;
let bigger = money3 >= money4;
assert!;
let smaller = money4 < money3;
assert!;
let smaller = money4 <= money3;
assert!;
Requires feature
raw_money
use raw;
use ;
let money = from_decimal;
let money2 = raw!;
let same = money == money2;
assert!;
let money3 = raw!;
// let same = money == money3; // compile error
let money4 = raw!;
let bigger = money3 > money4;
assert!;
let bigger = money3 >= money4;
assert!;
let smaller = money4 < money3;
assert!;
let smaller = money4 <= money3;
assert!;
is_approx
Check equality with tolerance. The tolerance is inclusive.
use money;
use ;
// approximation comparison
let money = money!;
let money2 = money!;
let same = money.is_approx;
assert!;
Requires feature
raw_money
use raw;
use ;
// approximation comparison
let money = raw!;
let money2 = raw!;
let same = money.is_approx;
assert!;
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 ;
let money = money!;
let money2 = money!;
let ret = money + money2;
let ret = ret - dec!;
let ret = / dec!;
assert_eq!;
let ret = ret - -money!;
assert_eq!;
let mut m = money!;
let m2 = money!;
m += m2;
m -= money!;
m *= money!;
m /= money!;
assert_eq!;
let money = money!;
let rem = money % dec!;
assert_eq!;
For raw money:
Requires feature
raw_money
use ;
let money = raw!;
let money2 = raw!;
let ret = money + money2;
let ret = ret - dec!;
let ret = / dec!;
assert_eq!;
let ret = ret - raw!;
assert_eq!;
let mut m = raw!;
let m2 = raw!;
m += m2;
m -= raw!;
m *= raw!;
m /= raw!;
assert_eq!;
let money = raw!;
let rem = money % dec!;
assert_eq!;
Negative sign:
You can flexibly put the sign in front of type or the amount.
use money;
use ;
let money = new.unwrap;
let money2 = -new.unwrap;
assert_eq!;
let money = new.unwrap;
let money2 = -new.unwrap;
assert_eq!;
let money = money!;
let money2 = -money!;
assert_eq!;
let money = money!;
let money2 = -money!;
assert_eq!;
For raw money:
Requires feature
raw_money
use raw;
use ;
let money = new.unwrap;
let money2 = -new.unwrap;
assert_eq!;
let money = new.unwrap;
let money2 = -new.unwrap;
assert_eq!;
let money = raw!;
let money2 = -raw!;
assert_eq!;
let money = raw!;
let money2 = -raw!;
assert_eq!;
Checked Arithmetic
use ;
let money = money!;
let money2 = money!;
let ret = money.checked_add.unwrap;
assert_eq!;
let ret = ret.checked_div.unwrap;
assert_eq!;
let ret = ret.checked_mul.unwrap;
assert_eq!;
let ret =
.checked_mul
.unwrap;
assert_eq!;
let money = money!;
let rem = money.checked_rem.unwrap;
assert_eq!;
For raw money:
Requires feature
raw_money
use ;
let money = raw!;
let money2 = raw!;
let ret = money.checked_add.unwrap;
assert_eq!;
let ret = ret.checked_div.unwrap;
assert_eq!;
let ret = ret.checked_mul.unwrap;
assert_eq!;
let ret =
.checked_mul
.unwrap;
assert_eq!;
let money = raw!;
let rem = money.checked_rem.unwrap;
assert_eq!;
Split & Allocation
split
Split money without losing a single penny returning equal split and remainder(if any).
use ;
let money = money!;
let split = 3;
let = money.split.unwrap;
assert_eq!;
assert!;
assert_eq!;
let money = -money!;
let split = 3;
let = money.split.unwrap;
assert_eq!;
assert!;
assert_eq!;
let money = money!;
let split = 3;
let = money.split.unwrap;
assert_eq!;
assert_eq!;
let money = money!;
let split = 0;
let ret = money.split;
assert!;
let money = money!;
let split = 1;
let = money.split.unwrap;
assert_eq!;
assert!;
let money = money!;
let split = 4;
let = money.split.unwrap;
assert_eq!;
assert!;
For 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.Requires feature
raw_money
use ;
let money = raw!;
let split = 3;
let = money.split.unwrap;
assert_eq!;
assert!;
assert_eq!;
let money = -raw!;
let split = 3;
let = money.split.unwrap;
assert_eq!;
assert!;
assert_eq!;
let money = raw!;
let split = 3;
let = money.split.unwrap;
assert_eq!;
assert_eq!;
let money = raw!;
let split = 0;
let ret = money.split;
assert!;
let money = raw!;
let split = 1;
let = money.split.unwrap;
assert_eq!;
assert!;
let money = raw!;
let split = 4;
let = money.split.unwrap;
assert_eq!;
assert!;
split_dist
Split money without losing a single penny returning vector of equal parts where remainder distributed across parts.
use ;
let money = money!;
let split = 3;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let money = -money!;
let split = 3;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let money = money!;
let split = 3;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let money = money!;
let split = 0;
let parts = money.split_dist;
assert!;
let money = money!;
let split = 1;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let money = money!;
let split = 4;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
For raw money:
Requires feature
raw_money
use ;
let money = raw!;
let split = 3;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let money = -raw!;
let split = 3;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let money = raw!;
let split = 3;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let money = raw!;
let split = 0;
let parts = money.split_dist;
assert!;
let money = raw!;
let split = 1;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let money = raw!;
let split = 4;
let parts = money.split_dist.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
allocate
Allocate money by percentages. Percentages must sum into 100.
use ;
let money = money!;
let allocations = money.allocate.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = money!;
let allocations = money.allocate.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = money!; // rounded to 1
let allocations = money.allocate.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = money!;
let allocations = money.allocate.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
For raw money:
Requires feature
raw_money
use ;
let money = raw!;
let allocations = money.allocate.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = -raw!;
let allocations = money.allocate.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = raw!;
let allocations = money.allocate.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = raw!;
let ret = money.allocate.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
allocate_by_ratios
Allocate money by ratios.
use ;
let money = money!;
let allocations = money.allocate_by_ratios.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = -money!;
let allocations = money.allocate_by_ratios.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = money!;
let allocations = money.allocate_by_ratios.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = money!;
let ret = money.allocate_by_ratios.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
For raw money:
Requires feature
raw_money
use ;
let money = raw!;
let allocations = money.allocate_by_ratios.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = -raw!;
let allocations = money.allocate_by_ratios.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = raw!;
let allocations = money.allocate_by_ratios.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
let money = raw!;
let ret = money.allocate_by_ratios.unwrap;
let expected = &;
assert_eq!;
assert_eq!;
Formatter
Formatting money into string for display. There are 2 formatter, one fromFormatting RawMoney requires feature
raw_money
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 ;
// BaseMoney formatter
let money = money!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
let money = -raw!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
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
nfor displaying negative money.
malways display money in minor amount.
use ;
let money = money!;
// Basic formatting
// "USD 100.50"
assert_eq!;
// "$100.50"
assert_eq!;
assert_eq!;
// "USD 10,050 ¢" (amount in minor units when 'm' is present)
assert_eq!;
// adding `n` to positive money will be ignored
assert_eq!;
// Mixing literals with format symbols
// "Total: $100.50"
assert_eq!;
// Escaping format symbols to display them as literals
// "a=100.50, c=USD"
assert_eq!;
let negative = -money!;
// "USD -50.00"
assert_eq!;
// "-$50.00"
assert_eq!;
// not specifying the `n` for negative sign will omit the negative sign.
assert_eq!;
assert_eq!;
assert_eq!;
// raw money
let money = -raw!;
let ret = money.format;
assert_eq!;
MoneyFormatter<C> format_with_separator
Same with format, only this allow you to customize the separators for thousands and decimal.
use ;
let money = money!;
let ret = money.format_with_separator;
assert_eq!;
let money = money!;
let ret = money.format_with_separator;
assert_eq!;
let money = raw!;
let ret = money.format_with_separator;
assert_eq!;
let money = raw!;
let ret = money.format_with_separator;
assert_eq!;
MoneyFormatter<C> format_locale_amount
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.Requires feature
locale
List of BCP 47 extension numbers support: icu4x (Look for "digits")
use ;
let money = money!;
assert_eq!;
let money = money!;
assert_eq!;
// Arabic (Saudi Arabia) locale: Arabic numerals
let money = money!;
assert_eq!;
// Arabic (Persians) locale: Arabic numerals
let money = money!;
assert_eq!;
// Negative amount: include `n` in format_str to show the negative sign
let money = money!;
assert_eq!;
// Indian numbers and group formatting.
let money = raw!;
let result = money.format_locale_amount;
assert_eq!;
// with locale numbers using BCP 47 extension
let money = raw!;
let result = money.format_locale_amount;
assert_eq!;
let money = money!;
let result = money
.format_locale_amount
.unwrap;
assert_eq!;
let money = money!;
let result = money.format_locale_amount.unwrap;
assert_eq!;
let money = money!;
let result = money.format_locale_amount.unwrap;
assert_eq!;
let money = raw!;
let result = money.format_locale_amount.unwrap;
assert_eq!;
let money = raw!;
let result = money
.format_locale_amount
.unwrap;
assert_eq!;
// Invalid locale returns an error
let money = raw!;
assert!;
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 ;
let moneys = vec!;
let sum = moneys.iter.;
assert_eq!;
let sum = moneys.into_iter.;
assert_eq!;
// raw money
let moneys = vec!;
let sum = moneys.iter.;
assert_eq!;
let sum = moneys.into_iter.;
assert_eq!;
IterOps<C>
This trait supports more operations such: checked_sum, mean, median, mode.
use ;
let moneys = vec!;
let sum = moneys.checked_sum.unwrap;
assert_eq!;
// raw money
let moneys = vec!;
let sum = moneys.checked_sum.unwrap;
assert_eq!;
use ;
let moneys = vec!;
let mean = moneys.mean.unwrap;
assert_eq!;
let median = moneys.median.unwrap;
assert_eq!;
let modes = moneys.mode.unwrap;
assert_eq!;
// raw money
let moneys = vec!;
let mean = moneys.mean.unwrap;
assert_eq!;
let median = moneys.median.unwrap;
assert_eq!;
let modes = moneys.mode.unwrap;
assert_eq!;
Percentage
Do some percentage operations. Supported operations:
percent: Calculates what a certain percentage of a money amount equals.percent_add: Adds amount by percentagepercent_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 ;
// money
// Calculate how much percentage of money equals to
let money = money!;
let tax = money.percent.unwrap; // 15% of $200
assert_eq!;
// Returns None on overflow
let none_on_overflow = money.percent;
assert!;
// Add money to a percentage of it
let price = money!;
let after_tax = price.percent_add.unwrap; // $100 + 20% = $120
assert_eq!;
// Returns None on overflow
let none_on_overflow = price.percent_add;
assert!;
// Add money to multiple percentages fixed to original amount.
let base = money!;
// 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.unwrap;
assert_eq!;
// Returns None on overflow
let none_on_overflow = base.percent_adds_fixed;
assert!;
// Add money to multiple percentages compounding.
let base = money!;
// 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.unwrap;
assert_eq!;
// Returns None on overflow
let none_on_overflow = base.percent_adds_compound;
assert!;
// Substract money by percentage of it
let price = money!;
let after_discount = price.percent_sub.unwrap; // $200 - 25% = $150
assert_eq!;
// Returns None on overflow
let none_on_overflow = price.percent_sub;
assert!;
// Substract money by multiple percentages in sequence.
let gross = money!;
// 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.unwrap;
assert_eq!;
// Returns None on overflow
let none_on_overflow = gross.percent_subs_sequence;
assert!;
// Determines what percentage one money is of another.
let profit = money!;
let revenue = money!;
let margin_percentage = profit.percent_of.unwrap; // $50 is 25% of $200
assert_eq!;
// Returns None when dividing by zero
let zero = money!;
assert!;
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 currencyRawMoney<T>where T is target currencyDecimalf64i32i64i128ExchangeRates<'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:
| Currencies | Rates | Pair |
|---|---|---|
| USD(base) | 1 | USD/USD |
| IDR | 17,000 | USD/IDR |
| EUR | 0.8 | USD/EUR |
| CAD | 1.2 | USD/CAD |
| JPY | 160 | USD/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 ;
// conversion with single rate
let usd = money!;
let usd_idr_rate = money!;
let idr_from_5usd = usd..unwrap;
assert_eq!;
// convert to self, rate will be ignored since self will be returned immediately.
let usd = money!;
let another_usd = usd..unwrap;
assert_eq!;
let usd = money!;
let idr_from_5usd = usd..unwrap;
assert_eq!;
// 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 = new;
rates.set;
rates.set;
rates.set;
rates.set;
rates.set;
rates.set;
rates.set; // ignored, since base already in USD, USD/USD stays 1.
let usd = money!;
let usd_chf = usd..unwrap;
assert_eq!;
let usd_irr = usd..unwrap;
assert_eq!;
// we can convert any currency to any currency as long as it exists in the rates.
let idr_usd = money!..unwrap;
assert_eq!;
let cad_sgd: = money!.convert.unwrap;
assert_eq!;
let eur_irr: = money!.convert.unwrap;
assert_eq!;
let hkd_idr: = money!.convert;
assert!; // because HKD is not in the rates
// you can initialize the exchange rates like this:
let rates = from;
let usd = money!;
let usd_chf = usd..unwrap;
assert_eq!;
let usd_irr = usd..unwrap;
assert_eq!;
// we can convert any currency to any currency as long as it exists in the rates.
let idr_usd = money!..unwrap;
assert_eq!;
let cad_sgd: = money!.convert.unwrap;
assert_eq!;
let eur_irr: = money!.convert.unwrap;
assert_eq!;
let hkd_idr: = money!.convert;
assert!; // 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.
- serialize into string of
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.
- serialize into string of
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.
- serialize into string of
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.
- serialize into string of
str_code:- serialize into string of
<CODE> <AMOUNT>with locale separators. - deserialize from string of
<CODE> <AMOUNT>with locale separators.
- serialize into string of
str_symbol:- serialize into string of
<SYMBOL><AMOUNT>with locale separators. - deserialize from string of
<SYMBOL><AMOUNT>with locale separators.
- serialize into string of
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 ;
// ---------------------------------------------------------------------------
// 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) ─────────────────────────────
// ── nested sub-struct using RawMoney<C> (full precision) ───────────────────
// ── top-level order struct ──────────────────────────────────────────────────
// ── build the order in memory ───────────────────────────────────────────────
let order = Order ;
// ── serialize ───────────────────────────────────────────────────────────────
let result = to_value;
assert!;
let json = result.unwrap;
// ── top-level Money assertions (default → JSON Number) ─────────────────────
assert_eq!;
use Number;
use FromStr;
// Money default → number
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
// omitted_opt_usd → None → serializes as null for option_comma_str_code
assert_eq!;
// ── top-level RawMoney assertions (default → JSON Number) ──────────────────
// RawMoney default → number, full precision preserved
assert_eq!;
assert_eq!;
assert_eq!;
// raw_omitted_opt_usd → None → null
assert_eq!;
// ── PaymentDetails (Money, formatted strings) ───────────────────────────────
// comma_str_code: "USD 2,500.00"
assert_eq!;
// comma_str_symbol: "$150.75"
assert_eq!;
// dot_str_code: "EUR 1.234,56"
assert_eq!;
// dot_str_symbol: "€500,00"
assert_eq!;
// minor: 98000 (integer, 0-decimal JPY)
assert_eq!;
// comma_str_code: "BHD 1,234.567"
assert_eq!;
// option_comma_str_symbol, Some: "£75.00"
assert_eq!;
// option_dot_str_code, None: null
assert_eq!;
// option_minor, None: null
assert_eq!;
// ── RawLineItems (RawMoney, formatted strings) ──────────────────────────────
// comma_str_code: "USD 999.99875" (no thousands separator needed for 3-digit integer part)
assert_eq!;
// dot_str_symbol: "€1.234,56789"
assert_eq!;
// dot_str_code: "BHD 0,12345" (0 integer part, comma as decimal separator)
assert_eq!;
// minor: 500 (integer, 0-decimal JPY)
assert_eq!;
// option_comma_str_symbol, Some: "£12.34567"
assert_eq!;
// option_comma_str_code, None: null
assert_eq!;
// option_dot_str_code, None: null
assert_eq!;
// ── CHF locale-aware str_code / str_symbol ──────────────────────────────────
// str_code: CHF uses apostrophe as thousands separator → "CHF 1'234.57"
assert_eq!;
// str_symbol: "₣1'234.56789"
assert_eq!;
// ── round-trip sanity: deserialize back and compare key fields ──────────────
let round_tripped: Order = from_value.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert!;
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert!;
assert!;
assert_eq!;
assert_eq!;
Deserialization
use ;
// ---------------------------------------------------------------------------
// 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) ─────────────────────────────
// ── nested sub-struct using RawMoney<C> (full precision, no rounding) ──────
// ── top-level order struct ──────────────────────────────────────────────────
// ── 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 = ;
assert!;
let order = order.unwrap;
// ── Money top-level assertions ──────────────────────────────────────────────
assert_eq!;
// 9999.9951 → rounded to 2 d.p. (bankers) → 10000.00
assert_eq!;
assert_eq!;
// -3000 i64 → EUR, no fractional part
assert_eq!;
assert_eq!;
// "500.50" string-number → USD, exact
assert_eq!;
assert_eq!;
// omitted Money field → zero IDR
assert_eq!;
assert_eq!;
// omitted optional Money → None
assert!;
// ── RawMoney top-level assertions ───────────────────────────────────────────
// 9999.9951 → RawMoney keeps full precision, no rounding
assert_eq!;
assert_eq!;
// "1234.56789" → RawMoney preserves all 5 decimal places
assert_eq!;
assert_eq!;
// omitted RawMoney field → zero IDR
assert_eq!;
assert_eq!;
// omitted optional RawMoney → None
assert!;
// ── Money nested PaymentDetails assertions ──────────────────────────────────
// subtotal_usd — comma+code, 2-decimal USD, rounded
assert_eq!;
assert_eq!;
// shipping_usd — comma+symbol, 2-decimal USD
assert_eq!;
assert_eq!;
// tax_eur — dot+code, European format EUR
assert_eq!;
assert_eq!;
// discount_eur — dot+symbol, "€500,00" → 500.00
assert_eq!;
assert_eq!;
// fee_jpy — minor integer, 0-decimal JPY: 98000 → ¥98,000
assert_eq!;
assert_eq!;
assert_eq!;
// duty_bhd — comma+code, 3-decimal BHD
assert_eq!;
assert_eq!;
assert_eq!;
// tip_gbp — option_comma_str_symbol GBP, present
assert!;
assert_eq!;
assert_eq!;
// refund_eur — option_dot_str_code EUR, explicit null
assert!;
// bonus_jpy — option_minor JPY, omitted → None
assert!;
// ── RawMoney nested RawLineItems assertions ─────────────────────────────────
// unit_price_usd — comma+code, full precision USD (no rounding)
assert_eq!;
assert_eq!;
// verify .finish() would round to 2 d.p.
assert_eq!;
// tax_rate_eur — dot+symbol, full 5 d.p. EUR preserved
assert_eq!;
assert_eq!;
// .finish() rounds to 2 d.p.
assert_eq!;
// spread_bhd — dot+code BHD, 5 d.p. (more than BHD's 3 minor unit)
assert_eq!;
assert_eq!;
// .finish() rounds to 3 d.p. (BHD minor unit)
assert_eq!;
// surcharge_jpy — minor integer, 0-decimal JPY: 500 → ¥500
assert_eq!;
assert_eq!;
assert_eq!;
// commission_gbp — option_comma_str_symbol GBP, full precision, present
assert!;
assert_eq!;
assert_eq!;
// .finish() rounds to 2 d.p.
assert_eq!;
// rebate_usd — option_comma_str_code USD, explicit null
assert!;
// adjustment_eur — option_dot_str_code EUR, omitted → None
assert!;
assert_eq!;
assert_eq!;
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 ;
// -------------------------------------------------------------------------
// 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!;
let inv_a = account_a
.interest_fixed
.unwrap
.yearly
.year
.month
.day
.years;
// .returns()
assert_eq!;
assert_eq!;
// .future_value()
assert_eq!;
// .future_value() after 20% tax
assert_eq!;
// .present_value() round-trip
let fv_a = inv_a.future_value.unwrap;
let pv_a = fv_a
.interest_fixed
.unwrap
.yearly
.year
.month
.day
.years
.present_value
.unwrap;
assert_eq!;
// .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!;
assert_eq!;
// =========================================================================
// 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!;
// 2-month slice for returns / FV / PV
let inv_b_short = account_b
.interest_fixed
.unwrap
.yearly
.year
.month
.day
.rate_days
.months;
// .returns()
assert_eq!;
assert_eq!;
// .future_value()
assert_eq!;
// .present_value() round-trip
let fv_b = inv_b_short.future_value.unwrap;
let pv_b = fv_b
.interest_fixed
.unwrap
.yearly
.year
.month
.day
.rate_days
.months
.present_value
.unwrap;
assert_eq!;
// .payment() on full 30-year mortgage
let pmt_b = account_b
.interest_fixed
.unwrap
.yearly
.year
.month
.day
.rate_days
.months
.payment
.unwrap;
assert_eq!;
assert_eq!;
// =========================================================================
// 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!;
let contribs_c = ;
let inv_c = account_c
.interest_compound
.unwrap
.daily
.year
.month
.day
.days
.with_contribs
.unwrap;
// .returns()
assert_eq!;
assert_eq!;
// .future_value()
assert_eq!;
// .future_value() after 10% tax
assert_eq!;
// .present_value() round-trip (clean 2-day compounding, no contribs)
let inv_c_clean = account_c
.interest_compound
.unwrap
.daily
.year
.month
.day
.days;
let fv_c = inv_c_clean.future_value.unwrap;
let pv_c = fv_c
.interest_compound
.unwrap
.daily
.year
.month
.day
.days
.present_value
.unwrap;
assert_eq!;
// .payment()
// PMT = 500000 × 0.01 × (1.01)^5 / [(1.01)^5 − 1]
let pmt_c = account_c
.interest_fixed
.unwrap
.daily
.year
.month
.day
.days
.payment
.unwrap;
assert_eq!;
assert_eq!;
// minor_unit check: JPY has 0 decimal places
assert_eq!;
// =========================================================================
// 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!;
let inv_d_quarterly = account_d
.interest_compound
.unwrap
.yearly
.year
.month
.day
.rate_days
.quarters;
// .returns()
assert_eq!;
assert_eq!;
// .future_value()
assert_eq!;
// .present_value() round-trip
let fv_d_q = inv_d_quarterly.future_value.unwrap;
let pv_d_q = fv_d_q
.interest_compound
.unwrap
.yearly
.year
.month
.day
.rate_days
.quarters
.present_value
.unwrap;
assert_eq!;
// .payment()
// PMT = 10000 × 0.02 × (1.02)^4 / [(1.02)^4 − 1]
let pmt_d_q = account_d
.interest_fixed
.unwrap
.yearly
.year
.month
.day
.rate_days
.quarters
.payment
.unwrap;
assert_eq!;
assert_eq!;
// --- 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
.unwrap
.yearly
.year
.month
.day
.rate_days
.semi_annuals;
// .returns()
assert_eq!;
assert_eq!;
// .future_value()
assert_eq!;
// .present_value() round-trip
let fv_d_s = inv_d_semi.future_value.unwrap;
let pv_d_s = fv_d_s
.interest_compound
.unwrap
.yearly
.year
.month
.day
.rate_days
.semi_annuals
.present_value
.unwrap;
assert_eq!;
// .payment()
// PMT = 10000 × 0.04 × (1.04)^2 / [(1.04)^2 − 1]
let pmt_d_s = account_d
.interest_fixed
.unwrap
.yearly
.year
.month
.day
.rate_days
.semi_annuals
.payment
.unwrap;
assert_eq!;
assert_eq!;
// minor_unit check: BHD has 3 decimal places
assert_eq!;
// quarterly compound always beats semi-annual (more compounding periods)
assert!;
// =========================================================================
// Cross-cutting: compound always beats fixed for the same inputs
// =========================================================================
let principal = money!;
let fixed_returns = principal
.interest_fixed
.unwrap
.yearly
.year
.month
.day
.years
.returns
.unwrap;
let compound_returns = principal
.interest_compound
.unwrap
.yearly
.year
.month
.day
.years
.returns
.unwrap;
assert!;