use std::{
collections::{BTreeMap, HashMap},
sync::OnceLock as StdOnceLock,
};
use chrono::{DateTime, Duration, Local, NaiveDate};
use tokio::sync::{OnceCell, RwLock, RwLockReadGuard};
use crate::{request, Currency, Decimal, Error, Exchange, ExchangeRates, Result};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HistoricalExchangeRates;
type HistoricalExchangeMap = BTreeMap<NaiveDate, ExchangeRates>;
type HistoricalExchangeLock = RwLock<HistoricalExchangeMap>;
fn local_now() -> NaiveDate
{
Local::now().naive_local().date()
}
impl HistoricalExchangeRates
{
pub(crate) async fn cached() -> Result<&'static HistoricalExchangeLock>
{
static CELL: OnceCell<HistoricalExchangeLock> = OnceCell::const_new();
static LAST_CHECK: StdOnceLock<RwLock<NaiveDate>> = StdOnceLock::new();
let cached = CELL
.get_or_try_init(|| async {
let map = Self::from_ecb().await?;
LAST_CHECK.set(local_now().into()).ok();
Result::Ok(RwLock::new(map))
})
.await?;
let now = local_now();
if LAST_CHECK.get_or_init(|| local_now().into()).read().await.signed_duration_since(now) >=
Duration::days(1)
{
let mut history = cached.write().await;
*history = Self::from_ecb().await?;
drop(history);
let mut last_check = LAST_CHECK.get_or_init(|| local_now().into()).write().await;
*last_check = now;
}
Ok(cached)
}
pub async fn exchange<E>(
date: Option<DateTime<Local>>,
currency: Currency,
exchangeable: E,
) -> E
where
E: Exchange,
{
Self::try_exchange(date, currency, exchangeable).await.unwrap()
}
pub fn exchange_from<E>(
history: &HistoricalExchangeMap,
date: Option<DateTime<Local>>,
currency: Currency,
exchangeable: E,
) -> E
where
E: Exchange,
{
Self::exchange_opt_from(history, date, currency, exchangeable).unwrap()
}
pub async fn exchange_opt<E>(
date: Option<DateTime<Local>>,
currency: Currency,
exchangeable: E,
) -> Option<E>
where
E: Exchange,
{
Self::try_exchange_opt(date, currency, exchangeable).await.unwrap()
}
pub fn exchange_opt_from<E>(
history: &HistoricalExchangeMap,
date: Option<DateTime<Local>>,
currency: Currency,
exchangeable: E,
) -> Option<E>
where
E: Exchange,
{
Self::get_ref_from(history, date).map(|rates| exchangeable.exchange(currency, rates))
}
async fn from_ecb() -> Result<HistoricalExchangeMap>
{
let csv =
request::get_unzipped("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.zip")
.await?;
Self::parse_csv(&csv)
}
pub async fn get(date: Option<DateTime<Local>>) -> Result<Option<ExchangeRates>>
{
let history = Self::history().await?;
Ok(Self::get_from(&history, date))
}
pub fn get_from(
history: &HistoricalExchangeMap,
date: Option<DateTime<Local>>,
) -> Option<ExchangeRates>
{
Self::get_ref_from(history, date).cloned()
}
pub fn get_ref_from(
history: &HistoricalExchangeMap,
date: Option<DateTime<Local>>,
) -> Option<&ExchangeRates>
{
let naive = date.map_or_else(local_now, |d| d.naive_local().date());
history
.range(..=naive)
.next_back()
.or_else(|| history.range(naive..).next())
.map(|(_, rates)| rates)
}
pub async fn history() -> Result<RwLockReadGuard<'static, HistoricalExchangeMap>>
{
let cached = Self::cached().await?;
Ok(cached.read().await)
}
pub async fn index(date: Option<DateTime<Local>>) -> ExchangeRates
{
Self::try_index(date).await.unwrap()
}
pub fn index_from(
history: &HistoricalExchangeMap,
date: Option<DateTime<Local>>,
) -> ExchangeRates
{
Self::index_ref_from(history, date).clone()
}
pub fn index_ref_from(
history: &HistoricalExchangeMap,
date: Option<DateTime<Local>>,
) -> &ExchangeRates
{
Self::get_ref_from(history, date).unwrap()
}
pub fn parse_csv(csv: &str) -> Result<HistoricalExchangeMap>
{
let mut lines = csv.lines().map(|line| line.split(','));
let headers: Vec<_> = lines
.next()
.map(|split| split.skip(1).map(Currency::reverse_lookup).collect())
.ok_or_else(|| Error::csv_row_missing("headers"))?;
Ok(lines.fold(BTreeMap::new(), |mut m, mut values| {
let date = values.next().and_then(|d| d.parse::<NaiveDate>().ok()).unwrap_or_default();
let mut rates = headers.iter().zip(values).fold(
ExchangeRates(HashMap::new()),
|mut rates, (header, value)| {
if let Some(c) = header
{
if let Ok(d) = value.parse::<Decimal>()
{
rates.0.insert(*c, d);
}
}
rates
},
);
rates.0.insert(Currency::Eur, 1.into());
m.insert(date, rates);
m
}))
}
pub async fn try_exchange<E>(
date: Option<DateTime<Local>>,
currency: Currency,
exchangeable: E,
) -> Result<E>
where
E: Exchange,
{
Self::try_exchange_opt(date, currency, exchangeable).await.map(Option::unwrap)
}
pub async fn try_exchange_opt<E>(
date: Option<DateTime<Local>>,
currency: Currency,
exchangeable: E,
) -> Result<Option<E>>
where
E: Exchange,
{
let history = Self::history().await?;
Ok(Self::exchange_opt_from(&history, date, currency, exchangeable))
}
pub async fn try_index(date: Option<DateTime<Local>>) -> Result<ExchangeRates>
{
Self::get(date)
.await
.map(|rates| rates.expect("The internal historical record has no data"))
}
}
#[cfg(test)]
mod tests
{
use pretty_assertions::assert_eq;
use super::{
Currency,
Decimal,
ExchangeRates,
HistoricalExchangeRates,
Local,
NaiveDate,
Result,
};
use crate::Money;
#[tokio::test]
async fn cached() -> Result<()>
{
let lock = HistoricalExchangeRates::cached().await?;
let history = lock.read().await;
let (date, rates) = history.first_key_value().unwrap();
assert_eq!(date, &NaiveDate::from_ymd_opt(1999, 01, 04).unwrap());
assert_eq!(
rates,
&ExchangeRates(
[
(Currency::Aud, Decimal::new(1_91, 2)),
(Currency::Cad, Decimal::new(1_8004, 4)),
(Currency::Chf, Decimal::new(1_6168, 4)),
(Currency::Czk, Decimal::new(35_107, 3)),
(Currency::Dkk, Decimal::new(7_4501, 4)),
(Currency::Eur, 1.into()),
(Currency::Gbp, Decimal::new(0_7111, 4)),
(Currency::Hkd, Decimal::new(9_1332, 4)),
(Currency::Huf, Decimal::new(251_48, 2)),
(Currency::Isk, Decimal::new(81_48, 2)),
(Currency::Jpy, Decimal::new(133_73, 2)),
(Currency::Krw, Decimal::new(1398_59, 2)),
(Currency::Nok, Decimal::new(8_855, 3)),
(Currency::Nzd, Decimal::new(2_2229, 4)),
(Currency::Pln, Decimal::new(4_0712, 4)),
(Currency::Sek, Decimal::new(9_4696, 4)),
(Currency::Sgd, Decimal::new(1_9554, 4)),
(Currency::Usd, Decimal::new(1_1789, 4)),
(Currency::Zar, Decimal::new(6_9358, 4)),
]
.into_iter()
.collect()
)
);
Ok(())
}
#[tokio::test]
async fn get() -> Result<()>
{
let mut after =
HistoricalExchangeRates::get(NaiveDate::from_ymd_opt(1999, 01, 04).and_then(|d| {
d.and_hms_opt(0, 0, 0).and_then(|dt| dt.and_local_timezone(Local).earliest())
}))
.await?;
let mut before =
HistoricalExchangeRates::get(NaiveDate::from_ymd_opt(1998, 01, 01).and_then(|d| {
d.and_hms_opt(0, 0, 0).and_then(|dt| dt.and_local_timezone(Local).earliest())
}))
.await?;
assert!(after.is_some());
assert_eq!(after, before);
after = HistoricalExchangeRates::get(NaiveDate::from_ymd_opt(2012, 05, 05).and_then(|d| {
d.and_hms_opt(0, 0, 0).and_then(|dt| dt.and_local_timezone(Local).earliest())
}))
.await?;
before =
HistoricalExchangeRates::get(NaiveDate::from_ymd_opt(2012, 05, 04).and_then(|d| {
d.and_hms_opt(0, 0, 0).and_then(|dt| dt.and_local_timezone(Local).earliest())
}))
.await?;
assert!(after.is_some());
assert_eq!(after, before);
Ok(())
}
#[tokio::test]
async fn exchange() -> Result<()>
{
let value = HistoricalExchangeRates::exchange(
None,
Default::default(),
Money::new(20_00, 2, Currency::Usd),
)
.await;
assert_eq!(value, Money::new(18_69, 2, Default::default()));
Ok(())
}
}