moneylib 0.4.0

Library to deal with money in Rust.
Documentation

moneylib

Rust Crates.io Documentation License: MIT

A library to deal with money safely using floating-point fixed-precision decimal.

Overview

moneylib provides a safe, robust, and ergonomic way to work with monetary value in Rust. It handles currency and amount with operations and arithmetics avoiding floating, rounding, and precision issue exist in typical binary floating-point type. It also make sure the money always in valid state on every operations and arithmetics done on it avoiding overflow/truncation/wrap and without fractions.

This crate uses Decimal type underneath for the amount of money.

Features

Here are some features supported:

  • Type-safe:
    • Compile-time check for arithmetics and operations.
    • Runtime check for overflowed/wrapped/truncated amount.
    • Prevents currencies mixing at compile-time.
  • Value type to represent money.
    • Money: represents money in amount rounded to the currency's minor unit.
    • RawMoney: represents money in raw amount keeping the precisions and choose when to round.
  • Access to its amount and currency's metadata.
  • Arithmetics: (*,/,+,-), operator overloading supported.
  • Comparisons: (>,<,>=,<=,==,!=), operator overloading supported.
  • Negative money.
  • Formatting and custom formatting.
  • Rounding with multiple strategies: Bankers rounding, half-up, half-down, ceil, and floor.
  • Money in form of its smallest amount (minor amount).
  • Some basic operations like absolute value, min, max, and clamp.
  • Support for all ISO 4217 currencies.
  • New/custom currency by implementing Currency trait.
  • Some accounting operations.(TODO)

Example

use moneylib::{Money, BaseMoney, BaseOps, CustomMoney, RoundingStrategy, USD, JPY, BHD, EUR, money_macros::dec};
use std::str::FromStr;

// Creating money from string (supports thousand separators)
let usd_money = Money::<USD>::from_str("USD 1,234.56").unwrap();
println!("{}", usd_money); // USD 1,234.56

// Creating money from minor amount (cents for USD)
let from_cents = Money::<USD>::from_minor(12345).unwrap();
println!("{}", from_cents); // USD 123.45

// Arithmetic operations with automatic rounding
let money_a = Money::<USD>::new(dec!(100.00)).unwrap();
let money_b = Money::<USD>::new(dec!(50.00)).unwrap();
println!("{}", money_a + money_b); // USD 150.00
println!("{}", money_a * dec!(1.5)); // USD 150.00
println!("{}", money_a / dec!(3)); // USD 33.33 (rounded)

// Comparisons
println!("{}", money_a > money_b); // true
println!("{}", money_a == Money::<USD>::new(dec!(100.00)).unwrap()); // true

// Working with different currencies
// JPY has 0 decimal places
let jpy_money = Money::<JPY>::new(dec!(1000)).unwrap();
println!("{}", jpy_money); // JPY 1,000

// BHD has 3 decimal places
let bhd_money = Money::<BHD>::new(dec!(12.345)).unwrap();
println!("{}", bhd_money); // BHD 12.345

// Custom formatting
let money = Money::<USD>::new(dec!(1234.56)).unwrap();
println!("{}", money.format_symbol()); // $1,234.56
println!("{}", money.format_code()); // USD 1,234.56

// Rounding with round_with method
let rounded = Money::<USD>::new(dec!(123.456)).unwrap();
let half_up_rounded = rounded.round_with(2, RoundingStrategy::HalfUp);
println!("{}", half_up_rounded.amount()); // 123.46

// Negative amounts
let negative = Money::<USD>::new(dec!(-50.00)).unwrap();
println!("{}", negative); // USD -50.00
println!("{}", negative.abs()); // USD 50.00

// Error handling with Result types
match money_a.add(money_b) {
    Ok(sum) => println!("Sum: {}", sum),
    Err(e) => println!("Error: {:?}", e),
}

// Safe operations with different currencies (won't compile due to type safety)
let eur_money = Money::<EUR>::new(dec!(100.00)).unwrap();
// This won't compile because USD and EUR are different types:
// let result = money_a + eur_money; // Compile error!

Components

This library provides these main components to work with money:

  • Money<C>: represents the money itself and all operations on it. Generic over currency type C.
  • Currency: trait that defines currency behavior and metadata. Implemented by currency marker types (e.g., USD, EUR, JPY).
  • Decimal: 128 bit floating-point with fixed-precision decimal number. Re-export from rust_decimal represents main type for money's amount.
  • BaseMoney: trait of money providing core operations and accessors.
  • BaseOps: trait for arithmetic and comparison operations on money.
  • CustomMoney: trait for custom formatting and rounding operations on money.
  • RoundingStrategy: enum defining rounding strategies (BankersRounding, HalfUp, HalfDown, Ceil, Floor).
  • MoneyError: enum of possible errors that can occur in money operations.

Money<C> and Decimal are Copy types so they can be passed around freely without having to worry about borrow checker. Currency marker types are zero-sized types (ZST) for compile-time type safety.

Invariants

Monetary values are sensitive matter and their invariants must always hold true.

Decimal

  • Significand(m): -296 < m < 296
  • Decimal points(s): 0 <= s <= 28

Money

  • Always rounded to its currency's minor unit using bankers rounding after each creation and operation done on it.
  • Creating money from string only accepts currencies already defined in ISO 4217.
  • Comparisons: Currency type-safety is enforced at compile time. Operations between different currencies won't compile.
  • Arithmetics:
    • *,+,-: will PANIC if overflowed. Currency mismatches are prevented at compile time.
    • /: will PANIC if overflowed or division by zero. Currency mismatches are prevented at compile time.
    • Use methods in BaseOps for non-panic arithmetics.

Currency

  • Currency types are defined at compile time using marker types (e.g., USD, EUR, JPY).
  • All ISO 4217 currencies are supported via the Currency trait.
  • Currency information is available through trait methods: code(), symbol(), name(), minor_unit().
  • New/custom currency is supported by implementing Currency trait.

This library maintains type-safety by preventing invalid state either by returning Result or going PANIC.

Feature Flags

raw_money

Enables the RawMoney<C> type which doesn't do automatic rounding like Money<C> does. It keeps full decimal precision and lets callers decide when to round.

[dependencies]
moneylib = { version = "0.4.0", features = ["raw_money"] }
use moneylib::{BaseMoney, RawMoney, USD, Money, money_macros::dec};

// RawMoney preserves all decimal precision
let raw = RawMoney::<USD>::new(dec!(100.567)).unwrap();
assert_eq!(raw.amount(), dec!(100.567)); // Not rounded!

// Convert from Money using into_raw()
let money = Money::<USD>::new(dec!(100.50)).unwrap();
let raw = money.into_raw();

// Perform precise calculations
let result = raw * dec!(1.08875); // Apply tax

// Convert back to Money with rounding using finish()
let final_money = result.finish();

Where rounding happens:

  • .round(): rounds to currency's minor unit using bankers rounding. Returns RawMoney.
  • .round_with(...): rounds using custom decimal points and strategy. Returns RawMoney.
  • .finish(): rounds to currency's minor unit using bankers rounding back to Money.

Code Coverage

This library maintains excellent code coverage.

To see current coverage results, run the coverage command below.

Running Code Coverage

To measure code coverage, you need to install cargo-tarpaulin:

cargo install cargo-tarpaulin

Then run coverage with:

cargo tarpaulin --out Stdout --out Html

This will:

  • Run all tests with coverage instrumentation
  • Display coverage results in the terminal
  • Generate an HTML report (tarpaulin-report.html) for detailed visualization