1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
use crate::currency::*;
use crate::{Money, MoneyError};
use rust_decimal::Decimal;
use std::collections::HashMap;

/// Stores a collection of exchange rates pairs between currencies.
#[derive(Debug, Default)]
pub struct Exchange {
    map: HashMap<String, ExchangeRate>,
}

impl Exchange {
    pub fn new() -> Exchange {
        Exchange {
            map: HashMap::new(),
        }
    }

    /// Update an ExchangeRate or add it if does not exist.
    pub fn add_or_update_rate(&mut self, rate: &ExchangeRate) {
        let key = Exchange::generate_key(rate.from, rate.to);
        self.map.insert(key, *rate);
    }

    /// Return the ExchangeRate given the currency pair.
    pub fn get_rate(self, from: &'static Currency, to: &'static Currency) -> Option<ExchangeRate> {
        let key = Exchange::generate_key(from, to);
        match self.map.get(&key) {
            Some(v) => Some(*v),
            None => None,
        }
    }

    fn generate_key(from: &'static Currency, to: &'static Currency) -> String {
        from.to_string() + "-" + &to.to_string()
    }
}

/// Stores rates and execute conversions between two currencies.
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct ExchangeRate {
    pub from: &'static Currency,
    pub to: &'static Currency,
    rate: Decimal,
}

impl ExchangeRate {
    pub fn new(
        from: &'static Currency,
        to: &'static Currency,
        rate: Decimal,
    ) -> Result<ExchangeRate, MoneyError> {
        if from == to {
            return Err(MoneyError::InvalidCurrency);
        }
        Ok(ExchangeRate { from, to, rate })
    }

    /// Converts a Money from one Currency to another using the exchange rate.
    pub fn convert(&self, amount: Money) -> Result<Money, MoneyError> {
        if amount.currency() != self.from {
            return Err(MoneyError::InvalidCurrency);
        }
        let converted_amount = amount.amount() * self.rate;
        Ok(Money::from_decimal(converted_amount, self.to))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::money;
    use crate::Iso::*;
    use rust_decimal_macros::*;

    #[test]
    fn exchange_stores_rates() {
        let usd = Currency::get(USD);
        let eur = Currency::get(EUR);
        let rate = ExchangeRate::new(usd, eur, dec!(1.5)).unwrap();

        let mut exchange = Exchange::new();
        exchange.add_or_update_rate(&rate);
        let fetched_rate = exchange.get_rate(usd, eur).unwrap();
        assert_eq!(fetched_rate.rate, dec!(1.5));
    }

    #[test]
    fn rate_convert() {
        let rate = ExchangeRate::new(Currency::get(USD), Currency::get(EUR), dec!(1.5)).unwrap();
        let amount = money!(10, "USD");
        let expected_amount = money!("15", "EUR");
        let converted_rate = rate.convert(amount).unwrap();
        assert_eq!(converted_rate, expected_amount);
    }

    #[test]
    fn rate_convert_errors_if_currencies_dont_match() {
        let rate =
            ExchangeRate::new(Currency::get(Iso::GBP), Currency::get(Iso::EUR), dec!(1.5)).unwrap();
        let amount = money!(10, "USD");

        assert_eq!(
            rate.convert(amount).unwrap_err(),
            MoneyError::InvalidCurrency,
        );
    }

    #[test]
    fn rate_new_errors_if_currencies_are_equal() {
        let rate = ExchangeRate::new(Currency::get(Iso::GBP), Currency::get(Iso::GBP), dec!(1.5));
        assert_eq!(rate.unwrap_err(), MoneyError::InvalidCurrency,);
    }
}