Skip to main content

Crate paft_money

Crate paft_money 

Source
Expand description

Currency and money primitives for the paft ecosystem.

Policy for ISO currencies without a minor-unit exponent (ISO-None):

  • If ISO 4217 defines a minor unit for an ISO currency, that exponent is used.
  • If ISO is silent (for example XAU, XDR), the crate consults the metadata registry by ISO code. If metadata is present, its minor_units is used.
  • If no metadata is registered, operations that require a scale return MoneyError::MetadataNotFound with the offending currency.

Registering metadata overlays: Use set_currency_metadata to register a human-friendly name and scale:

set_currency_metadata("XAU", "Gold", 3, "XAU", true, Locale::EnUs).unwrap();
set_currency_metadata("XDR", "SDR", 6, "XDR", true, Locale::EnUs).unwrap();
use paft_money::Locale;
set_currency_metadata("XAU", "Gold", 3, "XAU", true, Locale::EnUs).unwrap();
set_currency_metadata("XDR", "SDR", 6, "XDR", true, Locale::EnUs).unwrap();

Once a scale is known for a code, set_currency_metadata refuses to change minor_units; use override_currency_metadata when a scale change is intentional. Money captures the resolved scale at construction, so existing values are not reinterpreted by later registry changes. Serialized Money values also carry this captured minor_units scale. On deserialization, the serialized scale is validated against the amount and is enough to reconstruct values whose custom metadata is currently absent. If metadata is present but resolves to a different scale, deserialization fails rather than silently changing equality, hashing, or arithmetic compatibility. Metadata display fields are the source of truth for non-ISO currency names and for localized formatting metadata. ISO currencies keep their ISO 4217 name and, when ISO defines an exponent, their ISO minor-unit scale.

Using metals/funds (recommended defaults):

  • Gold XAU: 3 or 6 decimal places are common; choose per domain needs.
  • Silver XAG: similar; often 3.
  • Platinum XPT: often 3.
  • Special Drawing Rights XDR: 6 is common. These are recommendations; the appropriate scale is domain-driven. Always register the scale you need.

§Decimal backend

Decimal helpers live in the lightweight paft_decimal crate, which provides the paft_decimal::Decimal type, paft_decimal::RoundingStrategy, and supporting utilities used throughout paft. By default it uses rust_decimal providing 28 fractional digits of precision with a fast fixed-size representation. Alternatively, enabling the bigdecimal feature switches the backend to bigdecimal for effectively unbounded precision backed by big integers.

The public API, serde representation (amounts encoded as strings, currencies as ISO codes, and Money carrying its captured minor_units), and DataFrame integration remain stable across backends. The primary trade-offs are performance (the bigdecimal backend may allocate more often) and precision (see MAX_DECIMAL_PRECISION). Minor-unit scaling always uses 64-bit integers (10_i64.pow(scale)) and is therefore capped at 18 decimal places — see MAX_MINOR_UNIT_DECIMALS. Beyond that, the cap-line shift would push 10^scale outside i64. The minor-unit integer itself is widened to i128 before/after scaling, while each backend still enforces its own decimal representation limits.

§Currency value types

The ecosystem exposes complementary concrete types for different financial meanings:

let usd = Currency::Iso(IsoCurrency::USD);

// Quotes preserve provider precision beyond settlement minor units.
let quote = Price::from_canonical_str("1.3578", usd.clone())?;
let quantity = QuantityAmount::from_decimal(decimal::from_minor_units(250, 2)).unwrap();
let exact_total = quote.try_total(&quantity)?;

// Intermediate totals stay exact until settlement.
let adjustment = MonetaryAmount::from_canonical_str("0.0049", usd)?;
let subtotal = exact_total.try_add(&adjustment)?;

let settled = subtotal.to_money_with(
    RoundingStrategy::MidpointAwayFromZero,
    None,
)?;
assert_eq!(settled.format(), "3.4 USD");

§Quickstart

Create money in ISO currencies, add and subtract safely, serialize with stable representations, and convert via explicit exchange rates.

let price = Money::from_canonical_str("12.34", Currency::Iso(IsoCurrency::USD))?;
let tax   = Money::from_canonical_str("1.23",  Currency::Iso(IsoCurrency::USD))?;
let total = price.try_add(&tax)?;
assert_eq!(total.format(), "13.57 USD");

// Cross-currency addition is rejected
let eur = Money::from_canonical_str("5", Currency::Iso(IsoCurrency::EUR))?;
assert!(price.try_add(&eur).is_err());

§Currency conversion

Use an ExchangeRate to convert with explicit rounding.

let usd = Money::from_canonical_str("10.00", Currency::Iso(IsoCurrency::USD))?;
let rate = ExchangeRate::new(
    Currency::Iso(IsoCurrency::USD),
    Currency::Iso(IsoCurrency::EUR),
    Decimal::from(9) / Decimal::from(10), // 1 USD = 0.9 EUR
)?;
let eur = usd.try_convert_with(&rate, RoundingStrategy::MidpointAwayFromZero)?;
assert_eq!(eur.currency().code(), "EUR");

§Serde

Amounts serialize as strings (to avoid exponent notation), currencies serialize as their codes, and Money serializes the captured minor_units scale that participates in equality, hashing, and arithmetic compatibility. Example:

let usd = Money::from_canonical_str("12.34", Currency::Iso(IsoCurrency::USD)).unwrap();
let json = serde_json::to_string(&usd).unwrap();
assert_eq!(json, "{\"amount\":\"12.34\",\"currency\":\"USD\",\"minor_units\":2}");

Deserialization validates the amount against serialized minor_units. If metadata for the currency is currently registered and resolves to a different scale, the payload is rejected; if metadata is absent for a custom/ISO-None currency, the serialized scale preserves the captured settlement semantics.

§Currency metadata overlays

For ISO codes without a prescribed minor-unit exponent (e.g., XAU, XDR), register a scale so that rounding and minor-unit conversions are well-defined:

set_currency_metadata("XAU", "Gold", 3, "XAU", true, Locale::EnUs).unwrap();
set_currency_metadata("XDR", "SDR", 6, "XDR", true, Locale::EnUs).unwrap();
use paft_money::Locale;
set_currency_metadata("XAU", "Gold", 3, "XAU", true, Locale::EnUs).unwrap();
set_currency_metadata("XDR", "SDR", 6, "XDR", true, Locale::EnUs).unwrap();

Existing Money values retain the scale resolved at construction. Updating or clearing the process-local registry can affect future construction and formatting metadata, but not minor-unit conversion for values that already exist. Serialized Money values carry that retained scale as minor_units; conflicting current metadata is rejected at deserialization instead of silently changing the value’s identity. For modeled non-ISO currencies such as BTC, ETH, and XMR, metadata is also the source of truth for Currency::full_name(). ISO currency names are resolved from ISO 4217 even if metadata is registered for formatting.

§Feature flags

  • bigdecimal: switch to arbitrary precision decimals (slower, allocates for large values).
  • dataframe: enables serde/polars/df-derive-macros integration for dataframes.
  • panicking-money-ops: implements Add/Sub/Mul/Div for Money that assert on invalid operations. Prefer the try_* methods for fallible APIs.
  • money-formatting: opt-in locale-aware formatting and strict parsing for Money.

When money-formatting is enabled you opt into localized rendering explicitly:

let eur = Money::from_canonical_str("1234.56", Currency::Iso(IsoCurrency::EUR)).unwrap();
assert_eq!(format!("{eur}"), "1234.56 EUR"); // canonical display stays locale-neutral
assert_eq!(eur.format_with_locale(Locale::EnEu).unwrap(), "€1.234,56");
assert_eq!(
    Money::from_str_locale("€1.234,56", Currency::Iso(IsoCurrency::EUR), Locale::EnEu)
        .unwrap()
        .format(),
    "1234.56 EUR"
);
assert_eq!(
    format!("{}", eur.localized(Locale::EnEu).with_code()),
    "€1.234,56 EUR"
);

Regardless of backend, serde and the high-level API remain stable; see MAX_DECIMAL_PRECISION and MAX_MINOR_UNIT_DECIMALS for limits that affect scaling and minor-unit conversions.

Re-exports§

pub use currency::Currency;
pub use currency::OtherCurrency;
pub use currency_utils::CurrencyMetadata;
pub use currency_utils::MAX_DECIMAL_PRECISION;
pub use currency_utils::MAX_MINOR_UNIT_DECIMALS;
pub use currency_utils::MinorUnitError;
pub use currency_utils::clear_currency_metadata;
pub use currency_utils::currency_metadata;
pub use currency_utils::override_currency_metadata;
pub use currency_utils::set_currency_metadata;
pub use currency_utils::try_normalize_currency_code;
pub use error::MoneyError;
pub use error::MoneyParseError;
pub use money::ExchangeRate;
pub use money::Money;

Modules§

currency
Currency enumeration with ISO 4217 support and extensible fallback.
currency_utils
Utilities and helpers for working with Currency values.
error
Error types shared across the money crate.
money
Money type for representing financial values with currency.

Structs§

MonetaryAmount
Full-precision currency-denominated amount for totals and intermediate values.
Price
Full-precision currency price for per-unit quoted values.
PriceAmount
Full-precision price amount whose currency is supplied by surrounding context.
QuantityAmount
Full-precision non-negative quantity whose unit is supplied by surrounding context.

Enums§

IsoCurrency
Re-export iso_currency::Currency for convenience.
Locale
Supported locales for money formatting/parsing.