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, itsminor_unitsis used. - If no metadata is registered, operations that require a scale return
MoneyError::MetadataNotFoundwith 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:
paft_decimal: backend-agnostic helpers such aspaft_decimal::parse_decimal,paft_decimal::from_minor_units,paft_decimal::zero, andpaft_decimal::one.Money: settled or payable amounts that enforce currency exponents and settlement-ready rounding.Price: full-precision per-unit/per-share quoted values.MonetaryAmount: full-precision currency-denominated totals and intermediate values before final settlement rounding.QuantityAmount: full-precision non-negative market quantities whose unit is supplied by surrounding context.
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: enablesserde/polars/df-derive-macrosintegration for dataframes.panicking-money-ops: implementsAdd/Sub/Mul/DivforMoneythat assert on invalid operations. Prefer thetry_*methods for fallible APIs.money-formatting: opt-in locale-aware formatting and strict parsing forMoney.
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
Currencyvalues. - error
- Error types shared across the money crate.
- money
- Money type for representing financial values with currency.
Structs§
- Monetary
Amount - Full-precision currency-denominated amount for totals and intermediate values.
- Price
- Full-precision currency price for per-unit quoted values.
- Price
Amount - Full-precision price amount whose currency is supplied by surrounding context.
- Quantity
Amount - Full-precision non-negative quantity whose unit is supplied by surrounding context.
Enums§
- IsoCurrency
- Re-export
iso_currency::Currencyfor convenience. - Locale
- Supported locales for money formatting/parsing.