Skip to main content

Crate lucre

Crate lucre 

Source
Expand description

§lucre

status-badge Crates.io Version docs.rs

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:

TypeShapeAccepts
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
IsoNumericCode840an 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§

pub use rust_decimal::Decimal;
pub use vec1::Vec1;
pub use vec1::vec1;

Structs§

Balances
One Money per currency in a borrowed bag, ordered by ISO alphabetic code.
Currencies
One Currency per 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.
ExchangeRate
The rate at which one currency buys another.
Format
A reusable set of options for rendering monetary amounts, applied by Money::format_with.
IntoBalances
The same balances, out of a bag consumed whole.
IntoRates
The same rates, out of a table consumed whole.
IsoAlphabeticCode
The ISO 4217 alphabetic code for a currency.
IsoNumericCode
The ISO 4217 numeric code for a currency.
Money
An amount of a currency.
MoneyBag
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 Pair per 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 ExchangeRate per pair quoted, in pair order.

Enums§

ConvertError
An error from restating an amount at an ExchangeRate.
CrossRateError
An error from combining two ExchangeRates.
CurrencyError
An error from working with a Currency.
ExchangeError
An error from converting an amount against an Exchange.
ExchangeRateError
An error from quoting an ExchangeRate.
InvertError
An error from turning an ExchangeRate around.
IsoAlphabeticCodeError
An error from constructing an IsoAlphabeticCode.
IsoNumericCodeError
An error from constructing an IsoNumericCode.
MoneyBagError
An error from working with a MoneyBag.
MoneyError
An error from working with Money.
ParseCurrencyError
An error from reading a Currency from text.
ParseMoneyError
An error from reading a monetary amount from text.
ParsePairError
An error from reading a Pair out of text.
RoundingMode
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.