paft_money/lib.rs
1//! Currency and money primitives for the paft ecosystem.
2//!
3//! Policy for ISO currencies without a minor-unit exponent (ISO-None):
4//! - If ISO 4217 defines a minor unit for an ISO currency, that exponent is used.
5//! - If ISO is silent (for example `XAU`, `XDR`), the crate consults the metadata
6//! registry by ISO code. If metadata is present, its `minor_units` is used.
7//! - If no metadata is registered, operations that require a scale return
8//! `MoneyError::MetadataNotFound` with the offending currency.
9//!
10//! Registering metadata overlays:
11//! Use [`set_currency_metadata`] to register a human-friendly name and scale:
12//! ```rust
13//! # use paft_money::set_currency_metadata;
14//! # #[cfg(feature = "money-formatting")]
15//! # {
16//! # use paft_money::Locale;
17//! set_currency_metadata("XAU", "Gold", 3, "XAU", true, Locale::EnUs).unwrap();
18//! set_currency_metadata("XDR", "SDR", 6, "XDR", true, Locale::EnUs).unwrap();
19//! # }
20//! # #[cfg(not(feature = "money-formatting"))]
21//! # {
22//! use paft_money::Locale;
23//! set_currency_metadata("XAU", "Gold", 3, "XAU", true, Locale::EnUs).unwrap();
24//! set_currency_metadata("XDR", "SDR", 6, "XDR", true, Locale::EnUs).unwrap();
25//! # }
26//! ```
27//!
28//! Using metals/funds (recommended defaults):
29//! - Gold `XAU`: 3 or 6 decimal places are common; choose per domain needs.
30//! - Silver `XAG`: similar; often 3.
31//! - Platinum `XPT`: often 3.
32//! - Special Drawing Rights `XDR`: 6 is common. These are recommendations; the
33//! appropriate scale is domain-driven. Always register the scale you need.
34//!
35//! # Decimal backend
36//!
37//! The crate exposes a backend-agnostic [`Decimal`] type alongside
38//! [`RoundingStrategy`]. By default it uses
39//! [`rust_decimal`](https://docs.rs/rust_decimal) providing 28 fractional
40//! digits of precision with a fast fixed-size representation. Alternatively,
41//! enabling the `bigdecimal` feature switches the backend to
42//! [`bigdecimal`](https://docs.rs/bigdecimal) for effectively unbounded
43//! precision backed by big integers.
44//!
45//! The public API, serde representation (amounts encoded as strings, currencies
46//! as ISO codes), and `DataFrame` integration remain stable across backends. The
47//! primary trade-offs are performance (the `bigdecimal` backend may allocate
48//! more often) and precision (see [`MAX_DECIMAL_PRECISION`]). Minor-unit scaling
49//! always uses 64-bit integers and therefore remains capped at 18 decimal
50//! places so that `10^scale` fits inside an `i128` when performing
51//! conversions.
52//!
53//! # Quickstart
54//!
55//! Create money in ISO currencies, add and subtract safely, serialize with
56//! stable representations, and convert via explicit exchange rates.
57//!
58//! ```rust
59//! # use iso_currency::Currency as IsoCurrency;
60//! # use paft_money::{Currency, Money};
61//! # fn run() -> Result<(), paft_money::MoneyError> {
62//! let price = Money::from_canonical_str("12.34", Currency::Iso(IsoCurrency::USD))?;
63//! let tax = Money::from_canonical_str("1.23", Currency::Iso(IsoCurrency::USD))?;
64//! let total = price.try_add(&tax)?;
65//! assert_eq!(total.format(), "13.57 USD");
66//!
67//! // Cross-currency addition is rejected
68//! let eur = Money::from_canonical_str("5", Currency::Iso(IsoCurrency::EUR))?;
69//! assert!(price.try_add(&eur).is_err());
70//! # Ok(()) } run().unwrap();
71//! ```
72//!
73//! # Currency conversion
74//!
75//! Use an [`ExchangeRate`] to convert with explicit rounding.
76//!
77//! ```rust
78//! # use iso_currency::Currency as IsoCurrency;
79//! # use paft_money::{Currency, Money, ExchangeRate, Decimal, RoundingStrategy};
80//! # fn run() -> Result<(), paft_money::MoneyError> {
81//! let usd = Money::from_canonical_str("10.00", Currency::Iso(IsoCurrency::USD))?;
82//! let rate = ExchangeRate::new(
83//! Currency::Iso(IsoCurrency::USD),
84//! Currency::Iso(IsoCurrency::EUR),
85//! Decimal::from(9) / Decimal::from(10), // 1 USD = 0.9 EUR
86//! )?;
87//! let eur = usd.try_convert_with(&rate, RoundingStrategy::MidpointAwayFromZero)?;
88//! assert_eq!(eur.currency().code(), "EUR");
89//! # Ok(()) } run().unwrap();
90//! ```
91//!
92//! # Serde
93//!
94//! Amounts serialize as strings (to avoid exponent notation); currencies serialize
95//! as their codes. Example:
96//!
97//! ```rust
98//! # use iso_currency::Currency as IsoCurrency;
99//! # use paft_money::{Currency, Money};
100//! let usd = Money::from_canonical_str("12.34", Currency::Iso(IsoCurrency::USD)).unwrap();
101//! let json = serde_json::to_string(&usd).unwrap();
102//! assert_eq!(json, "{\"amount\":\"12.34\",\"currency\":\"USD\"}");
103//! ```
104//!
105//! # Currency metadata overlays
106//!
107//! For ISO codes without a prescribed minor-unit exponent (e.g., `XAU`, `XDR`),
108//! register a scale so that rounding and minor-unit conversions are well-defined:
109//!
110//! ```rust
111//! # use paft_money::set_currency_metadata;
112//! # #[cfg(feature = "money-formatting")]
113//! # {
114//! # use paft_money::Locale;
115//! set_currency_metadata("XAU", "Gold", 3, "XAU", true, Locale::EnUs).unwrap();
116//! set_currency_metadata("XDR", "SDR", 6, "XDR", true, Locale::EnUs).unwrap();
117//! # }
118//! # #[cfg(not(feature = "money-formatting"))]
119//! # {
120//! use paft_money::Locale;
121//! set_currency_metadata("XAU", "Gold", 3, "XAU", true, Locale::EnUs).unwrap();
122//! set_currency_metadata("XDR", "SDR", 6, "XDR", true, Locale::EnUs).unwrap();
123//! # }
124//! ```
125//!
126//! # Feature flags
127//!
128//! - `bigdecimal`: switch to arbitrary precision decimals (slower, allocates for large values).
129//! - `dataframe`: enables `serde`/`polars`/`df-derive` integration for dataframes.
130//! - `panicking-money-ops`: implements `Add`/`Sub`/`Mul`/`Div` for `Money` that
131//! assert on invalid operations. Prefer the `try_*` methods for fallible APIs.
132//! - `money-formatting`: opt-in locale-aware formatting and strict parsing for [`Money`].
133//!
134//! When `money-formatting` is enabled you opt into localized rendering explicitly:
135//! ```rust
136//! # #[cfg(feature = "money-formatting")] {
137//! # use iso_currency::Currency as IsoCurrency;
138//! # use paft_money::{Currency, Locale, Money};
139//! let eur = Money::from_canonical_str("1234.56", Currency::Iso(IsoCurrency::EUR)).unwrap();
140//! assert_eq!(format!("{eur}"), "1234.56 EUR"); // canonical display stays locale-neutral
141//! assert_eq!(eur.format_with_locale(Locale::EnEu).unwrap(), "€1.234,56");
142//! assert_eq!(
143//! Money::from_str_locale("€1.234,56", Currency::Iso(IsoCurrency::EUR), Locale::EnEu)
144//! .unwrap()
145//! .format(),
146//! "1234.56 EUR"
147//! );
148//! assert_eq!(
149//! format!("{}", eur.localized(Locale::EnEu).with_code()),
150//! "€1.234,56 EUR"
151//! );
152//! # }
153//! ```
154//!
155//! Regardless of backend, serde and the high-level API remain stable; see
156//! [`MAX_DECIMAL_PRECISION`] and [`MAX_MINOR_UNIT_DECIMALS`] for limits that
157//! affect scaling and minor-unit conversions.
158
159#![cfg_attr(docsrs, feature(doc_cfg))]
160#![forbid(unsafe_code)]
161#![warn(missing_docs)]
162#![allow(clippy::cargo_common_metadata)]
163
164#[cfg(feature = "money-formatting")]
165mod format;
166mod locale;
167#[cfg(feature = "money-formatting")]
168mod parser;
169
170pub mod currency;
171pub mod currency_utils;
172/// Decimal abstraction toggled by feature flags.
173pub mod decimal;
174/// Error types shared across the money crate.
175pub mod error;
176pub mod money;
177
178pub use currency::Currency;
179pub use currency_utils::{
180 MAX_DECIMAL_PRECISION, MAX_MINOR_UNIT_DECIMALS, MinorUnitError, clear_currency_metadata,
181 currency_metadata, set_currency_metadata, try_normalize_currency_code,
182};
183pub use decimal::{Decimal, RoundingStrategy};
184pub use error::{MoneyError, MoneyParseError};
185pub use locale::Locale;
186#[cfg(feature = "money-formatting")]
187pub use money::LocalizedMoney;
188pub use money::{ExchangeRate, Money};
189
190/// Re-export `iso_currency::Currency` for convenience.
191pub use iso_currency::Currency as IsoCurrency;