use crate::{Commodity, CommodityTypeID};
use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
#[cfg(feature = "serde-support")]
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ExchangeRateError {
#[error("The commodity type with id {0} is not present in the exchange rate.")]
CommodityTypeNotPresent(CommodityTypeID),
#[error("There was a divide overflow while computing the exchange rate, performing the division {0}/{1}.")]
DivideOverflow(Decimal, Decimal),
}
#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct ExchangeRate {
pub date: Option<NaiveDate>,
pub obtained_datetime: Option<DateTime<Utc>>,
pub base: Option<CommodityTypeID>,
pub rates: BTreeMap<CommodityTypeID, Decimal>,
}
impl ExchangeRate {
pub fn get_rate(&self, commodity_type_id: &CommodityTypeID) -> Option<&Decimal> {
self.rates.get(commodity_type_id)
}
pub fn convert(
&self,
commodity: Commodity,
target_commodity_type: CommodityTypeID,
) -> Result<Commodity, ExchangeRateError> {
if let Some(base) = self.base {
if commodity.type_id == base {
if let Some(rate) = self.get_rate(&target_commodity_type) {
return Ok(Commodity::new(
rate * commodity.value,
target_commodity_type,
));
};
}
if target_commodity_type == base {
if let Some(rate) = self.get_rate(&commodity.type_id) {
let div = commodity
.value
.checked_div(*rate)
.ok_or_else(|| ExchangeRateError::DivideOverflow(commodity.value, *rate))?;
return Ok(Commodity::new(div, target_commodity_type));
};
}
}
let commodity_rate = match self.get_rate(&commodity.type_id) {
Some(rate) => rate,
None => {
return Err(ExchangeRateError::CommodityTypeNotPresent(
commodity.type_id,
))
}
};
let target_rate = match self.get_rate(&target_commodity_type) {
Some(rate) => rate,
None => {
return Err(ExchangeRateError::CommodityTypeNotPresent(
target_commodity_type,
))
}
};
let div = commodity
.value
.checked_div(*commodity_rate)
.ok_or_else(|| ExchangeRateError::DivideOverflow(commodity.value, *commodity_rate))?;
let value = div * target_rate;
Ok(Commodity::new(value, target_commodity_type))
}
pub fn rate_between(
&self,
from: &CommodityTypeID,
to: &CommodityTypeID,
) -> Result<Option<Decimal>, ExchangeRateError> {
if let Some(base) = &self.base {
if from == base {
if let Some(rate) = self.get_rate(&to) {
return Ok(Some(*rate));
};
}
if to == base {
if let Some(rate) = self.get_rate(&from) {
let one = Decimal::new(1, 0);
return match one.checked_div(*rate) {
Some(value) => Ok(Some(value)),
None => Err(ExchangeRateError::DivideOverflow(one, *rate)),
};
};
}
}
let from_rate = match self.get_rate(&from) {
Some(rate) => rate,
None => return Ok(None),
};
let to_rate = match self.get_rate(&to) {
Some(rate) => rate,
None => return Ok(None),
};
match to_rate.checked_div(*from_rate) {
Some(value) => Ok(Some(value)),
None => Err(ExchangeRateError::DivideOverflow(*to_rate, *from_rate)),
}
}
}
#[cfg(test)]
mod tests {
use super::{Commodity, CommodityTypeID, ExchangeRate};
use chrono::NaiveDate;
use rust_decimal::Decimal;
use std::collections::BTreeMap;
use std::str::FromStr;
#[cfg(feature = "serde-support")]
#[test]
fn test_json_serialization() {
use serde_json;
let original_data = r#"{
"date": "2020-02-07",
"base": "AUD",
"rates": {
"USD": 2.542,
"EU": "1.234"
}
}
"#;
let exchange_rate: ExchangeRate = serde_json::from_str(original_data).unwrap();
let usd = CommodityTypeID::from_str("USD").unwrap();
let eu = CommodityTypeID::from_str("EU").unwrap();
assert_eq!(
NaiveDate::from_ymd(2020, 02, 07),
exchange_rate.date.unwrap()
);
assert_eq!("AUD", exchange_rate.base.unwrap());
assert_eq!(
Decimal::from_str("2.542").unwrap(),
*exchange_rate.get_rate(&usd).unwrap()
);
assert_eq!(
Decimal::from_str("1.234").unwrap(),
*exchange_rate.get_rate(&eu).unwrap()
);
let expected_serialized_data = r#"{
"date": "2020-02-07",
"obtained_datetime": null,
"base": "AUD",
"rates": {
"EU": "1.234",
"USD": "2.542"
}
}"#;
let serialized_data = serde_json::to_string_pretty(&exchange_rate).unwrap();
assert_eq!(expected_serialized_data, serialized_data);
}
#[test]
fn convert_reference_rates() {
let mut rates: BTreeMap<CommodityTypeID, Decimal> = BTreeMap::new();
let aud = CommodityTypeID::from_str("AUD").unwrap();
let nzd = CommodityTypeID::from_str("NZD").unwrap();
rates.insert(aud, Decimal::from_str("1.6417").unwrap());
rates.insert(nzd, Decimal::from_str("1.7094").unwrap());
let exchange_rate = ExchangeRate {
date: Some(NaiveDate::from_ymd(2020, 02, 07)),
base: None,
obtained_datetime: None,
rates,
};
{
let start_commodity = Commodity::new(Decimal::from_str("10.0").unwrap(), aud);
let converted_commodity = exchange_rate.convert(start_commodity, nzd);
assert_eq!(
Decimal::from_str("10.412377413656575501005055735").unwrap(),
converted_commodity.unwrap().value
);
assert_eq!(
exchange_rate.rate_between(&aud, &nzd).unwrap(),
Some(Decimal::from_str("1.0412377413656575501005055734").unwrap())
);
}
{
let start_commodity = Commodity::new(Decimal::from_str("10.0").unwrap(), nzd);
let converted_commodity = exchange_rate.convert(start_commodity, aud);
assert_eq!(
Decimal::from_str("9.603954603954603954603954604").unwrap(),
converted_commodity.unwrap().value
);
assert_eq!(
exchange_rate.rate_between(&nzd, &aud).unwrap(),
Some(Decimal::from_str("0.9603954603954603954603954603").unwrap())
);
}
}
#[test]
fn convert_base_rate() {
let mut rates: BTreeMap<CommodityTypeID, Decimal> = BTreeMap::new();
let nok = CommodityTypeID::from_str("NOK").unwrap();
let usd = CommodityTypeID::from_str("USD").unwrap();
let gel = CommodityTypeID::from_str("GEL").unwrap();
rates.insert(nok, Decimal::from_str("9.2691220713").unwrap());
rates.insert(gel, Decimal::from_str("3.08").unwrap());
let exchange_rate = ExchangeRate {
date: Some(NaiveDate::from_ymd(2020, 02, 07)),
base: Some(usd),
obtained_datetime: None,
rates,
};
{
let start_commodity = Commodity::new(Decimal::from_str("100.0").unwrap(), usd);
let converted_commodity = exchange_rate.convert(start_commodity, nok);
assert_eq!(
Decimal::from_str("926.91220713000").unwrap(),
converted_commodity.unwrap().value
);
assert_eq!(
exchange_rate.rate_between(&usd, &nok).unwrap(),
Some(Decimal::from_str("9.2691220713").unwrap())
);
}
{
let start_commodity = Commodity::new(Decimal::from_str("100.0").unwrap(), nok);
let converted_commodity = exchange_rate.convert(start_commodity, usd);
assert_eq!(
Decimal::from_str("10.788508256853169187585300627").unwrap(),
converted_commodity.unwrap().value
);
assert_eq!(
exchange_rate.rate_between(&nok, &usd).unwrap(),
Some(Decimal::from_str("0.1078850825685316918758530063").unwrap())
);
}
{
let start_commodity = Commodity::new(Decimal::from_str("100.0").unwrap(), nok);
let converted_commodity = exchange_rate.convert(start_commodity, gel);
assert_eq!(
Decimal::from_str("33.228605431107761097762725931").unwrap(),
converted_commodity.unwrap().value
);
assert_eq!(
exchange_rate.rate_between(&nok, &gel).unwrap(),
Some(Decimal::from_str("0.3322860543110776109776272593").unwrap())
);
}
}
}