use crate::currency::Currency;
use crate::errors::QlResult;
use crate::money::Money;
use crate::types::Real;
use crate::{fail, require};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ExchangeRateType {
Direct,
Derived,
}
#[derive(Clone, Debug)]
pub struct ExchangeRate {
source: Currency,
target: Currency,
rate: Real,
}
impl ExchangeRate {
pub fn new(source: Currency, target: Currency, rate: Real) -> Self {
Self {
source,
target,
rate,
}
}
pub fn checked_new(source: Currency, target: Currency, rate: Real) -> QlResult<Self> {
require!(
rate.is_finite() && rate > 0.0,
"exchange rate must be finite and positive"
);
Ok(Self::new(source, target, rate))
}
pub fn source(&self) -> &Currency {
&self.source
}
pub fn target(&self) -> &Currency {
&self.target
}
pub fn rate_type(&self) -> ExchangeRateType {
ExchangeRateType::Direct
}
pub fn rate(&self) -> Real {
self.rate
}
pub fn exchange(&self, amount: &Money) -> QlResult<Money> {
require!(
self.rate.is_finite() && self.rate > 0.0,
"exchange rate must be finite and positive"
);
require!(amount.value().is_finite(), "money amount must be finite");
if amount.currency() == &self.source {
Money::checked_new(self.target.clone(), amount.value() * self.rate)
} else if amount.currency() == &self.target {
Money::checked_new(self.source.clone(), amount.value() / self.rate)
} else {
fail!(
"exchange rate not applicable: money is {}, rate is {}/{}",
amount.currency().code(),
self.source.code(),
self.target.code()
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quantlib_direct_rate_exchanges_in_both_directions() {
let rate = ExchangeRate::new(Currency::eur(), Currency::usd(), 1.2042);
for amount in [100000.0, -100000.0, 0.0] {
let usd = rate.exchange(&Money::new(Currency::eur(), amount)).unwrap();
assert_eq!(usd.currency(), &Currency::usd());
assert!((usd.value() - amount * 1.2042).abs() < 1e-10);
let eur = rate.exchange(&usd).unwrap();
assert!((eur.value() - amount).abs() < 1e-10);
assert_eq!(eur.currency(), &Currency::eur());
}
assert_eq!(rate.rate_type(), ExchangeRateType::Direct);
}
#[test]
fn exchange_rejects_unrelated_currency_and_invalid_numbers() {
for rate in [0.0, -1.0, Real::NAN, Real::INFINITY] {
assert!(ExchangeRate::checked_new(Currency::eur(), Currency::usd(), rate).is_err());
assert!(
ExchangeRate::new(Currency::eur(), Currency::usd(), rate)
.exchange(&Money::new(Currency::eur(), 1.0))
.is_err()
);
}
let rate = ExchangeRate::new(Currency::eur(), Currency::usd(), 2.0);
assert!(rate.exchange(&Money::new(Currency::gbp(), 1.0)).is_err());
assert!(
rate.exchange(&Money::new(Currency::eur(), Real::MAX))
.is_err()
);
assert!(
rate.exchange(&Money::new(Currency::eur(), Real::NAN))
.is_err()
);
}
}