use std::{fmt, ops::Range, str::FromStr};
use miette::Diagnostic;
use thiserror::Error;
use crate::{
Currency, CurrencyError, Decimal, IsoAlphabeticCode, IsoAlphabeticCodeError, Money, MoneyError,
format::write_padded,
};
const SEPARATOR: char = '/';
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ExchangeRate {
pair: Pair,
rate: Decimal,
}
impl ExchangeRate {
pub fn new<P: Into<Pair>, R: Into<Decimal>>(
pair: P,
rate: R,
) -> Result<Self, ExchangeRateError> {
let rate = rate.into();
if rate <= Decimal::ZERO {
return Err(ExchangeRateError::InvalidRate { rate });
}
Ok(Self {
pair: pair.into(),
rate,
})
}
#[must_use]
pub fn identity(currency: Currency) -> Self {
Self {
pair: Pair::new(currency, currency),
rate: Decimal::ONE,
}
}
pub(super) fn new_unchecked(pair: Pair, rate: Decimal) -> Self {
Self { pair, rate }
}
#[must_use]
pub fn base(self) -> Currency {
self.pair.base()
}
#[must_use]
pub fn quote(self) -> Currency {
self.pair.quote()
}
#[must_use]
pub fn pair(self) -> Pair {
self.pair
}
#[must_use]
pub fn rate(self) -> Decimal {
self.rate
}
pub fn convert(self, money: Money) -> Result<Money, ConvertError> {
if money.currency() != self.base() {
return Err(ConvertError::CurrencyMismatch {
base: self.base(),
found: money.currency(),
});
}
let scaled = money
.checked_mul(self.rate)
.map_err(|source| ConvertError::Overflow {
pair: self.pair,
source,
})?;
Ok(Money::from_decimal(scaled.amount(), self.quote()))
}
pub fn cross_with(self, other: Self) -> Result<Self, CrossRateError> {
if self.quote() != other.base() {
return Err(CrossRateError::CurrencyMismatch {
first_quote: self.quote(),
second_base: other.base(),
});
}
let pair = Pair::new(self.base(), other.quote());
let rate = self
.rate
.checked_mul(other.rate)
.ok_or(CrossRateError::Overflow { pair })?;
Self::new(pair, rate).map_err(|source| CrossRateError::Rate { pair, source })
}
pub fn invert(self) -> Result<Self, InvertError> {
let inverted = Decimal::ONE
.checked_div(self.rate)
.filter(|inverted| inverted > &Decimal::ZERO)
.ok_or(InvertError::Underflow {
pair: self.pair,
rate: self.rate,
})?;
Ok(Self::new_unchecked(self.pair.invert(), inverted))
}
}
impl fmt::Display for ExchangeRate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_padded(f, &format!("{} {}", self.pair, self.rate))
}
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Pair {
base: Currency,
quote: Currency,
}
impl Pair {
#[must_use]
pub const fn new(base: Currency, quote: Currency) -> Self {
Self { base, quote }
}
#[must_use]
pub const fn base(self) -> Currency {
self.base
}
#[must_use]
pub const fn quote(self) -> Currency {
self.quote
}
#[must_use]
pub const fn invert(self) -> Self {
Self::new(self.quote, self.base)
}
}
impl From<(Currency, Currency)> for Pair {
fn from((base, quote): (Currency, Currency)) -> Self {
Self::new(base, quote)
}
}
impl From<&(Currency, Currency)> for Pair {
fn from(currencies: &(Currency, Currency)) -> Self {
Self::from(*currencies)
}
}
impl From<&Pair> for Pair {
fn from(pair: &Pair) -> Self {
*pair
}
}
impl From<Pair> for (Currency, Currency) {
fn from(Pair { base, quote }: Pair) -> Self {
(base, quote)
}
}
impl fmt::Display for Pair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { base, quote } = self;
write!(f, "{base}{SEPARATOR}{quote}")
}
}
impl fmt::Debug for Pair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Pair({self})")
}
}
impl FromStr for Pair {
type Err = ParsePairError;
fn from_str(notation: &str) -> Result<Self, Self::Err> {
let Some(slash) = notation.find(SEPARATOR) else {
return Err(ParsePairError::MissingSeparator {
notation: notation.to_string(),
at: 0..notation.len(),
});
};
Ok(Self {
base: read_currency(notation, Side::Base, 0..slash)?,
quote: read_currency(
notation,
Side::Quote,
slash + SEPARATOR.len_utf8()..notation.len(),
)?,
})
}
}
impl TryFrom<&str> for Pair {
type Error = ParsePairError;
fn try_from(notation: &str) -> Result<Self, Self::Error> {
notation.parse()
}
}
fn read_currency(notation: &str, side: Side, at: Range<usize>) -> Result<Currency, ParsePairError> {
let code = IsoAlphabeticCode::try_from(¬ation[at.clone()]).map_err(|source| {
ParsePairError::Code {
notation: notation.to_string(),
side,
at: at.clone(),
source,
}
})?;
Currency::try_from(code).map_err(|source| ParsePairError::UnknownCurrency {
notation: notation.to_string(),
side,
at,
source,
})
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Side {
Base,
Quote,
}
impl fmt::Display for Side {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Base => "base",
Self::Quote => "quote",
})
}
}
#[derive(Clone, Copy, Debug, Diagnostic, Error, Eq, PartialEq)]
#[diagnostic(url(docsrs))]
#[non_exhaustive]
pub enum ExchangeRateError {
#[error("an exchange rate must be above zero, but got {rate}")]
#[diagnostic(
code(lucre::exchange::rate::invalid),
help("to price the other direction, quote the inverse pair instead of a negative rate")
)]
#[non_exhaustive]
InvalidRate {
rate: Decimal,
},
}
#[derive(Clone, Debug, Diagnostic, Error, Eq, PartialEq)]
#[diagnostic(url(docsrs))]
#[non_exhaustive]
pub enum ConvertError {
#[error("the rate prices {base}, but the amount is in {found}")]
#[diagnostic(
code(lucre::exchange::convert::currency_mismatch),
help("invert the rate to price {found} instead, or convert the amount to {base} first")
)]
#[non_exhaustive]
CurrencyMismatch {
base: Currency,
found: Currency,
},
#[error("converting at {pair} gives an amount too large for a decimal")]
#[diagnostic(code(lucre::exchange::convert::overflow), forward(source))]
#[non_exhaustive]
Overflow {
pair: Pair,
source: MoneyError,
},
}
#[derive(Clone, Debug, Diagnostic, Error, Eq, PartialEq)]
#[diagnostic(url(docsrs))]
#[non_exhaustive]
pub enum ParsePairError {
#[error("a currency pair is two codes split by a slash, but got {notation:?}")]
#[diagnostic(
code(lucre::exchange::pair::missing_separator),
help("write the pair as `USD/EUR`")
)]
#[non_exhaustive]
MissingSeparator {
#[source_code]
notation: String,
#[label("no slash here")]
at: Range<usize>,
},
#[error("the {side} of the pair is not an ISO 4217 code")]
#[diagnostic(code(lucre::exchange::pair::code), forward(source))]
#[non_exhaustive]
Code {
#[source_code]
notation: String,
side: Side,
#[label("not an ISO 4217 code")]
at: Range<usize>,
source: IsoAlphabeticCodeError,
},
#[error("the {side} of the pair names no ISO 4217 currency")]
#[diagnostic(code(lucre::exchange::pair::unknown_currency), forward(source))]
#[non_exhaustive]
UnknownCurrency {
#[source_code]
notation: String,
side: Side,
#[label("names no currency")]
at: Range<usize>,
source: CurrencyError,
},
}
impl ParsePairError {
#[must_use]
pub fn notation(&self) -> &str {
match self {
Self::MissingSeparator { notation, .. }
| Self::Code { notation, .. }
| Self::UnknownCurrency { notation, .. } => notation,
}
}
#[must_use]
pub fn span(&self) -> Range<usize> {
match self {
Self::MissingSeparator { at, .. }
| Self::Code { at, .. }
| Self::UnknownCurrency { at, .. } => at.clone(),
}
}
}
#[derive(Clone, Copy, Debug, Diagnostic, Error, Eq, PartialEq)]
#[diagnostic(url(docsrs))]
#[non_exhaustive]
pub enum CrossRateError {
#[error("the first rate prices into {first_quote}, but the second prices {second_base}")]
#[diagnostic(
code(lucre::exchange::cross::currency_mismatch),
help("crossing needs the first rate to price into the currency the second prices")
)]
#[non_exhaustive]
CurrencyMismatch {
first_quote: Currency,
second_base: Currency,
},
#[error("the crossed {pair} rate is too large for a decimal")]
#[diagnostic(
code(lucre::exchange::cross::overflow),
help("a `Decimal` holds up to 28 significant digits")
)]
#[non_exhaustive]
Overflow {
pair: Pair,
},
#[error("the crossed {pair} rate cannot be quoted")]
#[diagnostic(code(lucre::exchange::cross::rate), forward(source))]
#[non_exhaustive]
Rate {
pair: Pair,
source: ExchangeRateError,
},
}
#[derive(Clone, Copy, Debug, Diagnostic, Error, Eq, PartialEq)]
#[diagnostic(url(docsrs))]
#[non_exhaustive]
pub enum InvertError {
#[error("the {pair} rate {rate} is too large to invert")]
#[diagnostic(
code(lucre::exchange::invert::underflow),
help("a rate below `2e28` inverts; quote the other direction directly instead")
)]
#[non_exhaustive]
Underflow {
pair: Pair,
rate: Decimal,
},
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::prelude::*;
fn usd_eur() -> ExchangeRate {
ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0.9)).unwrap()
}
#[test]
fn rate_refuses_a_zero_multiplier_test() {
assert_eq!(
ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0)),
Err(ExchangeRateError::InvalidRate { rate: dec!(0) })
);
}
#[test]
fn rate_refuses_a_negative_multiplier_test() {
assert_eq!(
ExchangeRate::new((Currency::USD, Currency::EUR), dec!(-0.9)),
Err(ExchangeRateError::InvalidRate { rate: dec!(-0.9) })
);
}
#[test]
fn refusal_names_the_multiplier_it_turned_away_test() {
let refused = ExchangeRate::new((Currency::USD, Currency::EUR), dec!(-0.9)).unwrap_err();
assert!(refused.to_string().contains("-0.9"));
}
#[test]
fn rate_quotes_a_pair_named_in_advance_test() {
let pair = Pair::new(Currency::USD, Currency::EUR);
assert_eq!(ExchangeRate::new(pair, dec!(0.9)), Ok(usd_eur()));
}
#[test]
fn rate_quotes_a_currency_against_itself_test() {
let rate = ExchangeRate::new((Currency::USD, Currency::USD), dec!(1)).unwrap();
assert_eq!(rate, ExchangeRate::identity(Currency::USD));
}
#[test]
fn identity_leaves_an_amount_alone_test() {
let fare = Money::from_minor(275, Currency::USD);
assert_eq!(
ExchangeRate::identity(Currency::USD).convert(fare),
Ok(fare)
);
}
#[test]
fn convert_multiplies_the_amount_test() {
assert_eq!(
usd_eur().convert(Money::from_major(100, Currency::USD)),
Ok(Money::from_major(90, Currency::EUR))
);
}
#[test]
fn convert_keeps_the_scale_multiplying_reached_test() {
let converted = usd_eur()
.convert(Money::from_minor(2550, Currency::USD))
.unwrap();
assert_eq!(converted.amount(), dec!(22.950));
assert_eq!(converted.amount().scale(), 3);
}
#[test]
fn convert_refuses_another_currency_test() {
assert_eq!(
usd_eur().convert(Money::from_major(10, Currency::GBP)),
Err(ConvertError::CurrencyMismatch {
base: Currency::USD,
found: Currency::GBP
})
);
}
#[test]
fn convert_reports_an_unrepresentable_product_test() {
let steep = ExchangeRate::new((Currency::USD, Currency::EUR), Decimal::MAX).unwrap();
assert_eq!(
steep.convert(Money::from_decimal(Decimal::MAX, Currency::USD)),
Err(ConvertError::Overflow {
pair: Pair::new(Currency::USD, Currency::EUR),
source: MoneyError::Overflow
})
);
}
#[test]
fn convert_overflow_names_the_pair_and_keeps_its_cause_test() {
let steep = ExchangeRate::new((Currency::USD, Currency::EUR), Decimal::MAX).unwrap();
let refused = steep
.convert(Money::from_decimal(Decimal::MAX, Currency::USD))
.unwrap_err();
assert_eq!(
refused.to_string(),
"converting at USD/EUR gives an amount too large for a decimal"
);
assert_eq!(
std::error::Error::source(&refused)
.map(ToString::to_string)
.as_deref(),
Some("the result is too large for a decimal")
);
}
#[test]
fn cross_spans_both_legs_test() {
let eur_jpy = ExchangeRate::new((Currency::EUR, Currency::JPY), dec!(160)).unwrap();
let usd_jpy = usd_eur().cross_with(eur_jpy).unwrap();
assert_eq!(usd_jpy.base(), Currency::USD);
assert_eq!(usd_jpy.quote(), Currency::JPY);
assert_eq!(usd_jpy.rate(), dec!(144));
}
#[test]
fn cross_refuses_rates_that_do_not_meet_test() {
let gbp_jpy = ExchangeRate::new((Currency::GBP, Currency::JPY), dec!(190)).unwrap();
assert_eq!(
usd_eur().cross_with(gbp_jpy),
Err(CrossRateError::CurrencyMismatch {
first_quote: Currency::EUR,
second_base: Currency::GBP
})
);
}
#[test]
fn cross_reports_a_product_too_large_to_hold_test() {
let steep = ExchangeRate::new((Currency::EUR, Currency::JPY), Decimal::MAX).unwrap();
let steeper = ExchangeRate::new((Currency::USD, Currency::EUR), Decimal::MAX).unwrap();
assert_eq!(
steeper.cross_with(steep),
Err(CrossRateError::Overflow {
pair: Pair::new(Currency::USD, Currency::JPY)
})
);
}
#[test]
fn cross_reports_a_vanished_product_as_an_invalid_rate_test() {
let slight =
ExchangeRate::new((Currency::USD, Currency::EUR), Decimal::new(1, 28)).unwrap();
let eur_jpy =
ExchangeRate::new((Currency::EUR, Currency::JPY), Decimal::new(1, 28)).unwrap();
let vanished = slight.cross_with(eur_jpy);
assert_eq!(
vanished,
Err(CrossRateError::Rate {
pair: Pair::new(Currency::USD, Currency::JPY),
source: ExchangeRateError::InvalidRate { rate: dec!(0) }
})
);
let refused = vanished.unwrap_err();
assert_eq!(
refused.to_string(),
"the crossed USD/JPY rate cannot be quoted"
);
assert_eq!(
std::error::Error::source(&refused)
.map(ToString::to_string)
.as_deref(),
Some("an exchange rate must be above zero, but got 0")
);
}
#[test]
fn cross_vanishes_against_an_everyday_rate_test() {
let slight =
ExchangeRate::new((Currency::USD, Currency::EUR), Decimal::new(1, 28)).unwrap();
let eur_jpy = ExchangeRate::new((Currency::EUR, Currency::JPY), dec!(0.5)).unwrap();
assert_eq!(
slight.cross_with(eur_jpy),
Err(CrossRateError::Rate {
pair: Pair::new(Currency::USD, Currency::JPY),
source: ExchangeRateError::InvalidRate { rate: dec!(0) }
})
);
}
#[test]
fn rate_displays_the_pair_and_the_multiplier_test() {
assert_eq!(usd_eur().to_string(), "USD/EUR 0.9");
assert_eq!(
ExchangeRate::identity(Currency::JPY).to_string(),
"JPY/JPY 1"
);
}
#[test]
fn rate_displays_the_multiplier_as_quoted_test() {
let thousandths = ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0.900)).unwrap();
assert_eq!(thousandths.to_string(), "USD/EUR 0.900");
}
#[test]
fn rate_display_honors_width_test() {
assert_eq!(format!("{:>13}", usd_eur()), " USD/EUR 0.9");
assert_eq!(format!("{:<13}", usd_eur()), "USD/EUR 0.9 ");
}
#[test]
fn pair_names_two_currencies_with_no_rate_in_hand_test() {
let pair = Pair::new(Currency::USD, Currency::EUR);
assert_eq!(pair.base(), Currency::USD);
assert_eq!(pair.quote(), Currency::EUR);
}
#[test]
fn pair_is_named_before_the_program_runs_test() {
const USD_EUR: Pair = Pair::new(Currency::USD, Currency::EUR);
const BASE: Currency = USD_EUR.base();
const QUOTE: Currency = USD_EUR.quote();
assert_eq!(BASE, Currency::USD);
assert_eq!(QUOTE, Currency::EUR);
}
#[test]
fn pair_converts_from_a_borrow_test() {
let quoted = [Pair::new(Currency::USD, Currency::EUR)];
for pair in "ed {
assert_eq!(Pair::from(pair), quoted[0]);
assert_eq!(ExchangeRate::new(pair, dec!(0.9)), Ok(usd_eur()));
}
}
#[test]
fn pair_reads_a_borrowed_tuple_test() {
let quoted = [(Currency::USD, Currency::EUR)];
for currencies in "ed {
assert_eq!(
Pair::from(currencies),
Pair::new(Currency::USD, Currency::EUR)
);
assert_eq!(ExchangeRate::new(currencies, dec!(0.9)), Ok(usd_eur()));
}
}
#[test]
fn pair_reads_a_tuple_in_the_order_a_board_speaks_test() {
assert_eq!(
Pair::from((Currency::USD, Currency::EUR)),
Pair::new(Currency::USD, Currency::EUR)
);
}
#[test]
fn pair_splits_back_into_the_two_currencies_it_names_test() {
let (base, quote) = Pair::new(Currency::USD, Currency::EUR).into();
assert_eq!(base, Currency::USD);
assert_eq!(quote, Currency::EUR);
}
#[test]
fn pair_displays_the_two_currencies_over_a_slash_test() {
assert_eq!(usd_eur().pair().to_string(), "USD/EUR");
}
#[test]
fn pairs_sort_by_the_currency_priced_then_the_one_it_is_priced_in_test() {
let mut pairs = [
Pair::new(Currency::USD, Currency::JPY),
Pair::new(Currency::EUR, Currency::USD),
Pair::new(Currency::USD, Currency::EUR),
];
pairs.sort();
assert_eq!(
pairs.map(|pair| pair.to_string()),
["EUR/USD", "USD/EUR", "USD/JPY"]
);
}
#[test]
fn pair_reads_back_what_it_writes_test() {
let pair = usd_eur().pair();
assert_eq!(pair.to_string().parse(), Ok(pair));
}
#[test]
fn pair_reads_text_whichever_conversion_a_caller_reaches_for_test() {
let pair = Pair::new(Currency::USD, Currency::EUR);
assert_eq!(Pair::try_from("USD/EUR"), Ok(pair));
assert_eq!("USD/EUR".parse(), Ok(pair));
}
#[test]
fn pair_refuses_currencies_with_nothing_between_them_test() {
assert_eq!(
"USDEUR".parse::<Pair>(),
Err(ParsePairError::MissingSeparator {
notation: "USDEUR".to_string(),
at: 0..6,
})
);
}
#[test]
fn pair_refuses_a_code_no_currency_bears_test() {
let unassigned = CurrencyError::UnknownAlphabeticCode {
code: "ZZZ".to_owned(),
};
assert_eq!(
"ZZZ/EUR".parse::<Pair>(),
Err(ParsePairError::UnknownCurrency {
notation: "ZZZ/EUR".to_string(),
side: Side::Base,
at: 0..3,
source: unassigned.clone(),
})
);
assert_eq!(
"USD/ZZZ".parse::<Pair>(),
Err(ParsePairError::UnknownCurrency {
notation: "USD/ZZZ".to_string(),
side: Side::Quote,
at: 4..7,
source: unassigned,
})
);
}
#[test]
fn refusal_leaves_the_unassigned_code_to_the_error_it_wraps_test() {
let refused = "USD/ZZZ".parse::<Pair>().unwrap_err();
assert_eq!(
std::error::Error::source(&refused)
.map(ToString::to_string)
.as_deref(),
Some(r#"no ISO 4217 currency uses the code "ZZZ""#)
);
}
#[test]
fn pair_reads_codes_as_iso_writes_them_test() {
assert_eq!(
"usd/EUR".parse::<Pair>(),
Err(ParsePairError::Code {
notation: "usd/EUR".to_string(),
side: Side::Base,
at: 0..3,
source: IsoAlphabeticCodeError::InvalidCode {
code: "usd".to_string()
},
})
);
assert_eq!(
"USD/eur".parse::<Pair>(),
Err(ParsePairError::Code {
notation: "USD/eur".to_string(),
side: Side::Quote,
at: 4..7,
source: IsoAlphabeticCodeError::InvalidCode {
code: "eur".to_string()
},
})
);
}
#[test]
fn pair_takes_the_first_separator_as_the_dividing_one_test() {
assert_eq!(
"USD/EUR/JPY".parse::<Pair>(),
Err(ParsePairError::Code {
notation: "USD/EUR/JPY".to_string(),
side: Side::Quote,
at: 4..11,
source: IsoAlphabeticCodeError::InvalidCode {
code: "EUR/JPY".to_string()
},
})
);
}
#[test]
fn refusals_name_the_text_they_turned_away_test() {
let unseparated = "USDEUR".parse::<Pair>().unwrap_err().to_string();
let misspelled = "usd/eur".parse::<Pair>().unwrap_err().to_string();
let unassigned = "USD/ZZZ".parse::<Pair>().unwrap_err().to_string();
assert_eq!(
unseparated,
r#"a currency pair is two codes split by a slash, but got "USDEUR""#
);
assert_eq!(misspelled, "the base of the pair is not an ISO 4217 code");
assert_eq!(
unassigned,
"the quote of the pair names no ISO 4217 currency"
);
}
#[test]
fn refusal_leaves_the_misspelling_to_the_error_it_wraps_test() {
let refused = "usd/EUR".parse::<Pair>().unwrap_err();
assert_eq!(
std::error::Error::source(&refused)
.map(ToString::to_string)
.as_deref(),
Some(r#"an alphabetic currency code is three capital letters, but got "usd""#)
);
}
#[test]
fn refusal_points_at_the_side_at_fault_test() {
let misspelled = "USD/eur".parse::<Pair>().unwrap_err();
let unassigned = "ZZZ/EUR".parse::<Pair>().unwrap_err();
assert_eq!(&misspelled.notation()[misspelled.span()], "eur");
assert_eq!(&unassigned.notation()[unassigned.span()], "ZZZ");
}
#[test]
fn refusal_takes_its_advice_from_the_error_it_wraps_test() {
let refused = "USD/ZZZ".parse::<Pair>().unwrap_err();
assert_eq!(
refused.help().map(|help| help.to_string()).as_deref(),
CurrencyError::UnknownAlphabeticCode {
code: "ZZZ".to_owned(),
}
.help()
.map(|help| help.to_string())
.as_deref()
);
}
#[test]
fn debug_spells_a_pair_test() {
assert_eq!(
format!("{:?}", Pair::new(Currency::USD, Currency::EUR)),
"Pair(USD/EUR)"
);
}
#[test]
fn inverting_a_pair_swaps_its_sides_test() {
assert_eq!(
Pair::new(Currency::USD, Currency::EUR).invert(),
Pair::new(Currency::EUR, Currency::USD)
);
}
#[test]
fn inverting_a_rate_prices_the_other_direction_test() {
let usd_eur = ExchangeRate::new((Currency::USD, Currency::EUR), dec!(0.8)).unwrap();
assert_eq!(
usd_eur.invert(),
Ok(ExchangeRate::new((Currency::EUR, Currency::USD), dec!(1.25)).unwrap())
);
}
#[test]
fn inverting_the_identity_rate_leaves_it_alone_test() {
let identity = ExchangeRate::identity(Currency::USD);
assert_eq!(identity.invert(), Ok(identity));
}
#[test]
fn inverting_twice_rounds_at_28_digits_test() {
let usd_eur = ExchangeRate::new((Currency::USD, Currency::EUR), dec!(3)).unwrap();
let round_trip = usd_eur.invert().unwrap().invert().unwrap();
assert_eq!(round_trip.pair(), usd_eur.pair());
assert_ne!(round_trip.rate(), usd_eur.rate());
}
#[test]
fn inverting_refuses_a_multiplier_that_divides_away_test() {
let huge = Decimal::from_str("20000000000000000000000000000").unwrap();
let usd_eur = ExchangeRate::new((Currency::USD, Currency::EUR), huge).unwrap();
assert_eq!(
usd_eur.invert(),
Err(InvertError::Underflow {
pair: Pair::new(Currency::USD, Currency::EUR),
rate: huge,
})
);
}
#[test]
fn inverting_holds_on_to_the_largest_multiplier_it_can_test() {
let large = Decimal::from_str("19999999999999999999999999999").unwrap();
let usd_eur = ExchangeRate::new((Currency::USD, Currency::EUR), large).unwrap();
assert!(usd_eur.invert().is_ok());
}
}