use std::{
collections::{BTreeMap, btree_map},
iter::FusedIterator,
};
use miette::Diagnostic;
use thiserror::Error;
use super::{ConvertError, ExchangeRate, Pair};
use crate::{Currency, Decimal, Money};
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct Exchange {
rates: BTreeMap<Pair, Decimal>,
}
impl Exchange {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_rate(&mut self, rate: ExchangeRate) -> Option<ExchangeRate> {
self.rates
.insert(rate.pair(), rate.rate())
.map(|displaced| ExchangeRate::new_unchecked(rate.pair(), displaced))
}
pub fn remove_rate<P: Into<Pair>>(&mut self, pair: P) -> Option<ExchangeRate> {
let pair = pair.into();
self.rates
.remove(&pair)
.map(|rate| ExchangeRate::new_unchecked(pair, rate))
}
pub fn clear(&mut self) {
self.rates.clear();
}
#[must_use]
pub fn rate<P: Into<Pair>>(&self, pair: P) -> Option<ExchangeRate> {
let pair = pair.into();
self.rates
.get(&pair)
.map(|&rate| ExchangeRate::new_unchecked(pair, rate))
}
pub fn convert(&self, money: Money, into: Currency) -> Result<Money, ExchangeError> {
Ok(self
.rate((money.currency(), into))
.ok_or(ExchangeError::Unquoted {
base: money.currency(),
quote: into,
})?
.convert(money)?)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rates.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.rates.len()
}
pub fn iter(&self) -> Rates<'_> {
Rates(self.rates.iter())
}
pub fn pairs(&self) -> Pairs<'_> {
Pairs(self.rates.keys())
}
}
#[derive(Debug, Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct Pairs<'a>(btree_map::Keys<'a, Pair, Decimal>);
impl Iterator for Pairs<'_> {
type Item = Pair;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().copied()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for Pairs<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back().copied()
}
}
impl ExactSizeIterator for Pairs<'_> {
fn len(&self) -> usize {
self.0.len()
}
}
impl FusedIterator for Pairs<'_> {}
#[derive(Debug, Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct Rates<'a>(btree_map::Iter<'a, Pair, Decimal>);
impl Iterator for Rates<'_> {
type Item = ExchangeRate;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(borrowed_entry_to_rate)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for Rates<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back().map(borrowed_entry_to_rate)
}
}
impl ExactSizeIterator for Rates<'_> {
fn len(&self) -> usize {
self.0.len()
}
}
impl FusedIterator for Rates<'_> {}
#[derive(Debug)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct IntoRates(btree_map::IntoIter<Pair, Decimal>);
impl Iterator for IntoRates {
type Item = ExchangeRate;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(entry_to_rate)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for IntoRates {
fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back().map(entry_to_rate)
}
}
impl ExactSizeIterator for IntoRates {
fn len(&self) -> usize {
self.0.len()
}
}
impl FusedIterator for IntoRates {}
fn entry_to_rate((pair, rate): (Pair, Decimal)) -> ExchangeRate {
ExchangeRate::new_unchecked(pair, rate)
}
fn borrowed_entry_to_rate((pair, rate): (&Pair, &Decimal)) -> ExchangeRate {
entry_to_rate((*pair, *rate))
}
impl IntoIterator for Exchange {
type Item = ExchangeRate;
type IntoIter = IntoRates;
fn into_iter(self) -> Self::IntoIter {
IntoRates(self.rates.into_iter())
}
}
impl<'a> IntoIterator for &'a Exchange {
type Item = ExchangeRate;
type IntoIter = Rates<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl Extend<ExchangeRate> for Exchange {
fn extend<I: IntoIterator<Item = ExchangeRate>>(&mut self, iter: I) {
for rate in iter {
self.set_rate(rate);
}
}
}
impl<'a> Extend<&'a ExchangeRate> for Exchange {
fn extend<I: IntoIterator<Item = &'a ExchangeRate>>(&mut self, iter: I) {
self.extend(iter.into_iter().copied());
}
}
impl FromIterator<ExchangeRate> for Exchange {
fn from_iter<I: IntoIterator<Item = ExchangeRate>>(iter: I) -> Self {
let mut desk = Self::new();
desk.extend(iter);
desk
}
}
impl<'a> FromIterator<&'a ExchangeRate> for Exchange {
fn from_iter<I: IntoIterator<Item = &'a ExchangeRate>>(iter: I) -> Self {
iter.into_iter().copied().collect()
}
}
#[derive(Clone, Debug, Diagnostic, Error, Eq, PartialEq)]
#[diagnostic(url(docsrs))]
#[non_exhaustive]
pub enum ExchangeError {
#[error("the table quotes no rate from {base} to {quote}")]
#[diagnostic(
code(lucre::exchange::unquoted),
help("set a rate for the pair, or cross two rates that meet at a shared currency")
)]
#[non_exhaustive]
Unquoted {
base: Currency,
quote: Currency,
},
#[error("the rate the table quotes could not convert the amount")]
#[diagnostic(code(lucre::exchange::convert), forward(source))]
#[non_exhaustive]
Convert {
#[from]
source: ConvertError,
},
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::prelude::*;
use std::{
collections::{HashSet, hash_map::DefaultHasher},
hash::{Hash, Hasher},
};
fn usd_eur() -> ExchangeRate {
ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0.9)).unwrap()
}
#[test]
fn exchange_answers_a_rate_it_was_given_test() {
let mut desk = Exchange::new();
desk.set_rate(usd_eur());
assert_eq!(desk.rate((Currency::USD, Currency::EUR)), Some(usd_eur()));
}
#[test]
fn exchange_answers_a_pair_named_in_advance_test() {
let mut desk = Exchange::new();
desk.set_rate(usd_eur());
assert_eq!(
desk.rate(Pair::new(Currency::USD, Currency::EUR)),
Some(usd_eur())
);
}
#[test]
fn exchange_takes_a_pair_by_borrow_test() {
let quoted = [Pair::new(Currency::USD, Currency::EUR)];
let mut desk = Exchange::new();
desk.set_rate(usd_eur());
for pair in "ed {
assert_eq!(desk.rate(pair), Some(usd_eur()));
assert_eq!(desk.remove_rate(pair), Some(usd_eur()));
}
}
#[test]
fn exchange_keeps_the_directions_of_a_pair_apart_test() {
let mut desk = Exchange::new();
desk.set_rate(usd_eur());
assert_eq!(desk.rate((Currency::EUR, Currency::USD)), None);
}
#[test]
fn exchange_displaces_an_earlier_rate_test() {
let revised = ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0.92)).unwrap();
let mut desk = Exchange::new();
desk.set_rate(usd_eur());
desk.set_rate(revised);
assert_eq!(desk.rate((Currency::USD, Currency::EUR)), Some(revised));
}
#[test]
fn exchange_hands_back_the_rate_it_displaces_test() {
let revised = ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0.92)).unwrap();
let mut desk = Exchange::new();
assert_eq!(desk.set_rate(usd_eur()), None);
assert_eq!(desk.set_rate(revised), Some(usd_eur()));
}
#[test]
fn exchange_takes_par_in_as_it_takes_any_pair_test() {
let par = ExchangeRate::identity(Currency::USD);
let mut desk = Exchange::new();
assert_eq!(desk.set_rate(par), None);
assert_eq!(desk.set_rate(par), Some(par));
}
#[test]
fn exchange_holds_nothing_to_begin_with_test() {
assert_eq!(Exchange::new().rate((Currency::USD, Currency::EUR)), None);
assert_eq!(Exchange::new().rate((Currency::USD, Currency::USD)), None);
}
#[test]
fn exchange_converts_at_the_rate_it_quotes_test() {
let desk = Exchange::from_iter([usd_eur()]);
assert_eq!(
desk.convert(Money::from_major(100, Currency::USD), Currency::EUR),
Ok(Money::from_major(90, Currency::EUR))
);
}
#[test]
fn exchange_refuses_to_convert_across_an_unquoted_self_pair_test() {
let fare = Money::from_minor(275, Currency::USD);
assert_eq!(
Exchange::new().convert(fare, Currency::USD),
Err(ExchangeError::Unquoted {
base: Currency::USD,
quote: Currency::USD,
})
);
}
#[test]
fn exchange_converts_at_a_quoted_self_pair_test() {
let discounted = ExchangeRate::new((Currency::USD, Currency::USD), dec!(0.99)).unwrap();
let desk = Exchange::from_iter([discounted]);
assert_eq!(desk.rate((Currency::USD, Currency::USD)), Some(discounted));
assert_eq!(
desk.convert(Money::from_minor(275, Currency::USD), Currency::USD),
Ok(Money::from_decimal(dec!(2.7225), Currency::USD))
);
}
#[test]
fn exchange_refuses_to_convert_across_an_unquoted_pair_test() {
let desk = Exchange::from_iter([usd_eur()]);
assert_eq!(
desk.convert(Money::from_major(100, Currency::USD), Currency::JPY),
Err(ExchangeError::Unquoted {
base: Currency::USD,
quote: Currency::JPY
})
);
}
#[test]
fn refusal_names_the_pair_it_could_not_price_test() {
let refused = ExchangeError::Unquoted {
base: Currency::USD,
quote: Currency::JPY,
};
assert!(refused.to_string().contains("USD"));
assert!(refused.to_string().contains("JPY"));
}
#[test]
fn exchange_reports_an_unrepresentable_conversion_test() {
let steep = ExchangeRate::new((Currency::USD, Currency::EUR), Decimal::MAX).unwrap();
let desk = Exchange::from_iter([steep]);
let fortune = Money::from_decimal(Decimal::MAX, Currency::USD);
let refused = desk.convert(fortune, Currency::EUR).unwrap_err();
assert!(matches!(refused, ExchangeError::Convert { .. }));
assert_eq!(
std::error::Error::source(&refused)
.map(ToString::to_string)
.as_deref(),
Some("converting at USD/EUR gives an amount too large for a decimal")
);
}
#[test]
fn exchange_is_empty_until_a_pair_is_quoted_test() {
let mut desk = Exchange::new();
assert!(desk.is_empty());
desk.set_rate(usd_eur());
assert!(!desk.is_empty());
}
#[test]
fn exchange_counts_one_rate_per_pair_test() {
let eur_usd = ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.1)).unwrap();
let revised = ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0.92)).unwrap();
let mut desk = Exchange::new();
assert_eq!(desk.len(), 0);
desk.set_rate(usd_eur());
desk.set_rate(eur_usd);
desk.set_rate(revised);
assert_eq!(desk.len(), 2);
}
#[test]
fn exchange_hands_back_the_rate_it_withdraws_test() {
let mut desk = Exchange::new();
desk.set_rate(usd_eur());
assert_eq!(
desk.remove_rate((Currency::USD, Currency::EUR)),
Some(usd_eur())
);
assert_eq!(desk.rate((Currency::USD, Currency::EUR)), None);
}
#[test]
fn exchange_withdraws_a_pair_named_in_advance_test() {
let mut desk = Exchange::new();
desk.set_rate(usd_eur());
assert_eq!(
desk.remove_rate(Pair::new(Currency::USD, Currency::EUR)),
Some(usd_eur())
);
}
#[test]
fn exchange_withdraws_one_direction_of_a_pair_test() {
let eur_usd = ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.1)).unwrap();
let mut desk = Exchange::from_iter([usd_eur(), eur_usd]);
desk.remove_rate((Currency::USD, Currency::EUR));
assert_eq!(desk.rate((Currency::EUR, Currency::USD)), Some(eur_usd));
}
#[test]
fn exchange_withdraws_nothing_from_an_unquoted_pair_test() {
let mut desk = Exchange::new();
assert_eq!(desk.remove_rate((Currency::USD, Currency::EUR)), None);
}
#[test]
fn exchange_clears_every_rate_test() {
let eur_usd = ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.1)).unwrap();
let mut desk = Exchange::from_iter([usd_eur(), eur_usd]);
desk.clear();
assert_eq!(desk, Exchange::new());
assert_eq!(desk.rate((Currency::USD, Currency::EUR)), None);
}
#[test]
fn exchange_iterates_in_pair_order_test() {
let usd_jpy = ExchangeRate::new((Currency::USD, Currency::JPY), dec!(144)).unwrap();
let eur_usd = ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.1)).unwrap();
let mut desk = Exchange::new();
desk.set_rate(usd_jpy);
desk.set_rate(usd_eur());
desk.set_rate(eur_usd);
assert_eq!(
desk.iter().collect::<Vec<_>>(),
[eur_usd, usd_eur(), usd_jpy]
);
}
#[test]
fn exchange_collects_from_rates_test() {
let eur_usd = ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.1)).unwrap();
let rates = [usd_eur(), eur_usd];
let owned: Exchange = rates.into_iter().collect();
let borrowed: Exchange = rates.iter().collect();
assert_eq!(owned.rate((Currency::USD, Currency::EUR)), Some(usd_eur()));
assert_eq!(owned.rate((Currency::EUR, Currency::USD)), Some(eur_usd));
assert_eq!(owned, borrowed);
}
#[test]
fn exchange_collects_a_later_rate_over_an_earlier_one_test() {
let revised = ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0.92)).unwrap();
let desk: Exchange = [usd_eur(), revised].into_iter().collect();
assert_eq!(desk.rate((Currency::USD, Currency::EUR)), Some(revised));
}
#[test]
fn exchange_extends_from_rates_test() {
let eur_usd = ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.1)).unwrap();
let usd_jpy = ExchangeRate::new((Currency::USD, Currency::JPY), dec!(144)).unwrap();
let mut desk = Exchange::from_iter([usd_eur()]);
desk.extend([eur_usd]);
desk.extend([&usd_jpy]);
assert_eq!(desk.rate((Currency::USD, Currency::EUR)), Some(usd_eur()));
assert_eq!(desk.rate((Currency::EUR, Currency::USD)), Some(eur_usd));
assert_eq!(desk.rate((Currency::USD, Currency::JPY)), Some(usd_jpy));
}
#[test]
fn into_iterator_yields_rates_test() {
let eur_usd = ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.1)).unwrap();
let desk: Exchange = [usd_eur(), eur_usd].into_iter().collect();
let borrowed: Vec<ExchangeRate> = (&desk).into_iter().collect();
let owned: Vec<ExchangeRate> = desk.into_iter().collect();
assert_eq!(owned, [eur_usd, usd_eur()]);
assert_eq!(owned, borrowed);
}
#[test]
fn rates_count_and_reverse_without_walking_forward_test() {
let eur_usd = ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.1)).unwrap();
let desk: Exchange = [usd_eur(), eur_usd].into_iter().collect();
assert_eq!(desk.iter().len(), 2);
assert_eq!(desk.iter().rev().collect::<Vec<_>>(), [usd_eur(), eur_usd]);
assert_eq!(
desk.into_iter().rev().collect::<Vec<_>>(),
[usd_eur(), eur_usd]
);
}
#[test]
fn equal_tables_hash_alike_test() {
let eur_usd = ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.1)).unwrap();
let one = Exchange::from_iter([usd_eur(), eur_usd]);
let other = Exchange::from_iter([eur_usd, usd_eur()]);
let hash_of = |desk: &Exchange| {
let mut hasher = DefaultHasher::new();
desk.hash(&mut hasher);
hasher.finish()
};
assert_eq!(one, other);
assert_eq!(hash_of(&one), hash_of(&other));
assert_eq!(HashSet::from([one, other]).len(), 1);
}
}