Expand description
§lucre
An ergonomic Rust library for handling money.
Represent money without generics or lifetimes, and without giving up safety or speed. ISO 4217 currency definitions are built in.
§Install
Add lucre to your Cargo.toml.
[dependencies]
lucre = "0.13.0"§Usage
Money is the main type. Currency supports it, and holds a constant for
every current ISO 4217 currency.
use lucre::{Money, Currency, Format, MoneyError};
fn main() -> Result<(), MoneyError> {
// Create money from major or minor units
let subtotal = Money::from_major(100, Currency::USD);
let tax = Money::from_minor(475, Currency::USD);
// Arithmetic comes in a checked form and a panicking one
let _unchecked = subtotal + tax;
let total = subtotal.checked_add(tax)?;
// Money displays with the code by default
assert_eq!(total.to_string(), "104.75 USD");
// A `Format` chooses something else
let format = Format::default().symbol();
assert_eq!(total.format_with(format).to_string(), "$104.75");
Ok(())
}§Several currencies at once
Money will not mix currencies: + panics and comparisons return None. To
carry amounts in more than one currency, use a MoneyBag, which holds a
separate balance for each.
use lucre::{Currency, Money, MoneyBag};
let mut wallet = MoneyBag::new();
wallet += Money::from_major(25, Currency::USD);
wallet += Money::from_major(10, Currency::EUR);
assert_eq!(
wallet.balance(Currency::USD),
Money::from_major(25, Currency::USD)
);
// A currency the bag has never held has a balance of zero
assert_eq!(
wallet.balance(Currency::JPY),
Money::from_major(0, Currency::JPY)
);
// The same iterator sums either way. The type you ask for decides:
// `Option<Money>` requires one currency, a bag allows several.
let refunds = [
Money::from_minor(1999, Currency::USD),
Money::from_minor(1250, Currency::USD),
];
assert_eq!(
refunds.iter().sum::<Option<Money>>(),
Some(Money::from_minor(3249, Currency::USD))
);
assert_eq!(refunds.iter().sum::<MoneyBag>().to_string(), "32.49 USD");
// If the currencies differ, only the bag sums
let mixed = [
Money::from_minor(1999, Currency::USD),
Money::from_minor(1250, Currency::EUR),
];
assert_eq!(
mixed.iter().sum::<MoneyBag>().to_string(),
"12.50 EUR, 19.99 USD"
);§Converting between currencies
Exchange rates change constantly, so lucre ships none of its own. Supply a rate
you already have and lucre does the arithmetic. An Exchange holds a set of
rates and converts amounts with them.
use std::error::Error;
use lucre::{Currency, Exchange, ExchangeRate, Money};
use rust_decimal::dec;
fn main() -> Result<(), Box<dyn Error>> {
let mut desk = Exchange::new();
desk.set_rate(ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0.9))?);
desk.set_rate(ExchangeRate::new((Currency::EUR, Currency::JPY), dec!(160))?);
let fare = Money::from_major(100, Currency::USD);
assert_eq!(desk.convert(fare, Currency::EUR)?, Money::from_major(90, Currency::EUR));
// A pair with no rate can be crossed from two rates sharing a currency
let usd_eur = desk
.rate((Currency::USD, Currency::EUR))
.ok_or("no rate for USD/EUR")?;
let eur_jpy = desk
.rate((Currency::EUR, Currency::JPY))
.ok_or("no rate for EUR/JPY")?;
let usd_jpy = usd_eur.cross_with(eur_jpy)?;
assert_eq!(usd_jpy.rate(), dec!(144.0));
// A conversion keeps every digit of the result. Round when you need to.
let converted = usd_jpy.convert(Money::from_minor(2599, Currency::USD))?;
assert_eq!(converted.amount(), dec!(3742.5600));
Ok(())
}Each rate applies in one direction. A rate for USD against EUR says nothing
about EUR against USD. Iterating a MoneyBag yields each currency’s balance in
ISO alphabetic order, so you can convert a whole bag one balance at a time.
A Pair is the two currencies without a rate.
use std::error::Error;
use lucre::{Currency, Pair};
fn main() -> Result<(), Box<dyn Error>> {
let watched: Pair = "USD/JPY".parse()?;
assert_eq!(watched.base(), Currency::USD);
assert_eq!(watched.quote(), Currency::JPY);
assert_eq!(watched.to_string(), "USD/JPY");
Ok(())
}§Features
§serde
Off by default. Turning it on gives Money, MoneyBag, ExchangeRate,
Exchange, Pair, Currency, Format, IsoAlphabeticCode,
IsoNumericCode, and RoundingMode a Serialize and a Deserialize impl.
[dependencies]
lucre = { version = "0.13.0", features = ["serde"] }Amounts and rates are written as text. Text keeps the fraction exact and keeps the scale the figure was built with. Numbers are read too, floats included, but only text survives a round trip unchanged.
{ "amount": "104.75", "currency": "USD" }A bag is one balance per currency, keyed by ISO alphabetic code. Reading adds up whatever the document says, rather than requiring it to match what a bag would have written. A balance of zero leaves no currency behind, and a currency named twice is summed.
{ "EUR": "10.00", "USD": "30.00" }A rate states its pair and the multiplier between them. The base is the
currency being priced and the quote is the currency it is priced in. A rate of
zero or less is rejected, as in ExchangeRate::new.
{ "base": "USD", "quote": "EUR", "rate": "0.9" }An Exchange is one rate per pair, keyed as BASE/QUOTE. Each direction is
its own entry, and a pair named twice keeps the rate given last.
{ "USD/EUR": "0.9", "EUR/USD": "1.1" }A Format is one field per option, so a program can read its rendering
conventions from a configuration file. A field the document leaves out keeps
the value Format::new starts with, and the three options that may be unset —
position, spaced, and precision — are left out when they are.
{
"identifier": "symbol",
"position": "prefix",
"spaced": false,
"negative": "parentheses",
"precision": { "digits": 2, "rounding": "half-up" },
"grouping": { "first": 3, "repeat": 2 },
"group_separator": ",",
"decimal_separator": "."
}The smaller types are single values rather than objects:
| Type | Shape | Accepts |
|---|---|---|
Currency | "USD" | the three-letter code, unassigned codes rejected |
Pair | "USD/EUR" | two codes split by a slash |
IsoAlphabeticCode | "ZZZ" | three capitals, assigned or not |
IsoNumericCode | 840 | an integer of at most three digits |
RoundingMode | "half-up" | or "half-down", or "half-even" |
Self-describing formats such as JSON, TOML, and YAML work. Formats without type information, such as bincode and postcard, do not.
§Maintainer
This project is maintained by Rosa Richter. For ways to contact her, see her contact page.
§Contributing
Questions and contributions are welcome. Please create an issue for bugs, feature requests, or questions.
§License
BSD-2-Clause-Patent © Rosa Richter
Re-exports§
Structs§
- Balances
- One
Moneyper currency in a borrowed bag, ordered by ISO alphabetic code. - Currencies
- One
Currencyper balance in a borrowed bag, ordered by ISO alphabetic code. - Currency
- An ISO 4217 currency.
- Exchange
- A set of exchange rates to look up by currency pair.
- Exchange
Rate - The rate at which one currency buys another.
- Format
- A reusable set of options for rendering monetary amounts, applied by
Money::format_with. - Into
Balances - The same balances, out of a bag consumed whole.
- Into
Rates - The same rates, out of a table consumed whole.
- IsoAlphabetic
Code - The ISO 4217 alphabetic code for a currency.
- IsoNumeric
Code - The ISO 4217 numeric code for a currency.
- Money
- An amount of a currency.
- Money
Bag - A balance in each of several currencies.
- Pair
- The two currencies a rate covers: the currency being priced, a slash, and the currency it is priced in.
- Pairs
- One
Pairper rate a borrowed table quotes, in pair order. - Parser
- A reusable set of options for reading monetary amounts from text.
- Rates
- What a borrowed table yields: one
ExchangeRateper pair quoted, in pair order.
Enums§
- Convert
Error - An error from restating an amount at an
ExchangeRate. - Cross
Rate Error - An error from combining two
ExchangeRates. - Currency
Error - An error from working with a
Currency. - Exchange
Error - An error from converting an amount against an
Exchange. - Exchange
Rate Error - An error from quoting an
ExchangeRate. - Invert
Error - An error from turning an
ExchangeRatearound. - IsoAlphabetic
Code Error - An error from constructing an
IsoAlphabeticCode. - IsoNumeric
Code Error - An error from constructing an
IsoNumericCode. - Money
BagError - An error from working with a
MoneyBag. - Money
Error - An error from working with
Money. - Parse
Currency Error - An error from reading a
Currencyfrom text. - Parse
Money Error - An error from reading a monetary amount from text.
- Parse
Pair Error - An error from reading a
Pairout of text. - Rounding
Mode - What to do with a value that falls exactly halfway when rounding.
- Side
- Which of a
Pair’s two currencies is meant: the one being priced, or the one it is priced in.