Convert between currencies with exchange rates #5

Merged
rosa merged 2 commits from feat/exchange-rates into main 2026-08-14 16:10:19 +00:00
Owner

Rates are live data rather than a standard, so nothing here quotes one for you.
ExchangeRate states a quote you already have, and Exchange keeps a set of
them to look up by currency pair.

Converting

  • ExchangeRate::new refuses a multiplier of zero or less, so a rate no
    exchange takes place at cannot be built.
  • convert restates a Money in the currency a rate prices it in, keeping
    every digit multiplying reached — rounding waits until the amount is due.
  • cross_with composes two quotes meeting at a shared currency into one
    spanning both legs, which is how a pair nobody quotes directly gets priced.
  • A currency converts to itself at par whether an Exchange says so or not.
    ExchangeRate::identity names that rate outright.
  • The two directions of a pair are separate entries, since a real market prices
    them separately, and a fresh quote displaces the last.
  • Exchange::quotes hands back every quote a table holds.

Serializing

Under the serde feature, a quote states the pair it spans and the multiplier
between them, and a table is a rate per pair, keyed the way a rate board names
one:

{ "from": "USD", "to": "EUR", "rate": "0.9" }
{ "USD/EUR": "0.9", "EUR/USD": "1.1" }

Reading either goes through ExchangeRate::new, so a document cannot state a
multiplier no exchange takes place at. A pair a table names twice keeps the
rate given last, matching what set_rate does.

Rates are live data rather than a standard, so nothing here quotes one for you. `ExchangeRate` states a quote you already have, and `Exchange` keeps a set of them to look up by currency pair. ## Converting - `ExchangeRate::new` refuses a multiplier of zero or less, so a rate no exchange takes place at cannot be built. - `convert` restates a `Money` in the currency a rate prices it in, keeping every digit multiplying reached — rounding waits until the amount is due. - `cross_with` composes two quotes meeting at a shared currency into one spanning both legs, which is how a pair nobody quotes directly gets priced. - A currency converts to itself at par whether an `Exchange` says so or not. `ExchangeRate::identity` names that rate outright. - The two directions of a pair are separate entries, since a real market prices them separately, and a fresh quote displaces the last. - `Exchange::quotes` hands back every quote a table holds. ## Serializing Under the `serde` feature, a quote states the pair it spans and the multiplier between them, and a table is a rate per pair, keyed the way a rate board names one: ```json { "from": "USD", "to": "EUR", "rate": "0.9" } { "USD/EUR": "0.9", "EUR/USD": "1.1" } ``` Reading either goes through `ExchangeRate::new`, so a document cannot state a multiplier no exchange takes place at. A pair a table names twice keeps the rate given last, matching what `set_rate` does.
`ExchangeRate` states a quote you already have, since rates are live data
rather than a standard and nothing here can source them. `Exchange` keeps a
set of them to look up by currency pair.

`convert` applies a rate to a `Money`, keeping every digit multiplying
reached so the rounding waits until the amount is due. `cross_with` composes
two rates meeting at a shared currency, which is how a pair nobody quotes
directly gets priced. Both report a mismatch or an unrepresentable result as
`MoneyError`, the vocabulary the crate already uses for those, leaving
`InvalidRateError` to say the one thing it does not: that a multiplier was
zero or negative.

A currency converts to itself at par whether the table says so or not, so
restating an amount in the currency it already carries never wants for a
rate. The two directions of a pair stay separate entries, since a real
market prices them separately.
feat: Serialize exchange rates and rate tables
All checks were successful
ci/woodpecker/push/test Pipeline was successful
6fc565a012
An ExchangeRate travels as the pair it spans and the multiplier between
them, and an Exchange as a rate per pair, keyed the way a rate board
names one. Reading a rate goes through ExchangeRate::new, so a document
cannot state a multiplier no exchange takes place at.

Exchange::quotes hands back every quote a table holds, which is what
serializing one needs and is worth having on its own.
rosa merged commit dc6bf8a971 into main 2026-08-14 16:10:19 +00:00
rosa deleted branch feat/exchange-rates 2026-08-14 16:10:19 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rosa/lucre!5
No description provided.