use std::cmp;
use std::collections::HashMap;
use serde::Deserialize;
use crate::api::{ApiClient, OutputSize, TimeSeriesInterval};
use crate::deserialize::from_str;
use crate::error::{Error, Result};
use crate::vec_trait::FindData;
#[derive(Debug, Clone, Default)]
struct MetaData {
information: String,
from_symbol: String,
to_symbol: String,
last_refreshed: String,
interval: Option<String>,
output_size: Option<String>,
time_zone: String,
}
#[derive(Default, Debug, Clone)]
pub struct Data {
time: String,
open: f64,
high: f64,
low: f64,
close: f64,
}
impl Data {
#[must_use]
pub fn time(&self) -> &str {
&self.time
}
#[must_use]
pub fn open(&self) -> f64 {
self.open
}
#[must_use]
pub fn high(&self) -> f64 {
self.high
}
#[must_use]
pub fn low(&self) -> f64 {
self.low
}
#[must_use]
pub fn close(&self) -> f64 {
self.close
}
}
#[derive(Debug, Default)]
pub struct Forex {
meta_data: MetaData,
data: Vec<Data>,
}
impl Forex {
#[must_use]
pub fn information(&self) -> &str {
self.return_meta_string("information")
}
#[must_use]
pub fn symbol_from(&self) -> &str {
self.return_meta_string("from symbol")
}
#[must_use]
pub fn symbol_to(&self) -> &str {
self.return_meta_string("to symbol")
}
#[must_use]
pub fn last_refreshed(&self) -> &str {
self.return_meta_string("last refreshed")
}
#[must_use]
pub fn time_zone(&self) -> &str {
self.return_meta_string("time zone")
}
#[must_use]
pub fn interval(&self) -> Option<&str> {
self.operate_option_meta_value("interval")
}
#[must_use]
pub fn output_size(&self) -> Option<&str> {
self.operate_option_meta_value("output size")
}
#[must_use]
pub fn data(&self) -> &Vec<Data> {
&self.data
}
fn return_meta_string(&self, which_val: &str) -> &str {
match which_val {
"information" => &self.meta_data.information,
"from symbol" => &self.meta_data.from_symbol,
"to symbol" => &self.meta_data.to_symbol,
"time zone" => &self.meta_data.time_zone,
"last refreshed" => &self.meta_data.last_refreshed,
_ => "",
}
}
fn operate_option_meta_value(&self, which_val: &str) -> Option<&str> {
let value = match which_val {
"interval" => &self.meta_data.interval,
"output size" => &self.meta_data.output_size,
_ => &None,
};
value.as_deref()
}
}
#[derive(Clone, Debug, Deserialize)]
struct DataHelper {
#[serde(rename = "1. open", deserialize_with = "from_str")]
open: f64,
#[serde(rename = "2. high", deserialize_with = "from_str")]
high: f64,
#[serde(rename = "3. low", deserialize_with = "from_str")]
low: f64,
#[serde(rename = "4. close", deserialize_with = "from_str")]
close: f64,
}
#[derive(Debug, Deserialize)]
pub(crate) struct ForexHelper {
#[serde(rename = "Meta Data")]
meta_data: Option<HashMap<String, String>>,
#[serde(flatten)]
forex: Option<HashMap<String, HashMap<String, DataHelper>>>,
}
impl ForexHelper {
fn convert(self) -> Result<Forex> {
if self.meta_data.is_none() || self.forex.is_none() {
return Err(Error::EmptyResponse);
}
let meta_data = self.meta_data.unwrap();
let information = &meta_data["1. Information"];
let from_symbol = &meta_data["2. From Symbol"];
let to_symbol = &meta_data["3. To Symbol"];
let mut last_refreshed = meta_data.get("4. Last Refreshed");
if last_refreshed.is_none() {
last_refreshed = meta_data.get("5. Last Refreshed");
};
let time_zone_value = meta_data.get("5. Time Zone").unwrap_or_else(|| {
meta_data.get("6. Time Zone").unwrap_or_else(|| {
meta_data
.get("7. Time Zone")
.expect("time zone contains None value")
})
});
let mut output_size_value = meta_data.get("4. Output Size");
if output_size_value.is_none() {
output_size_value = meta_data.get("6. Output Size");
}
let interval = meta_data.get("5. Interval");
let meta_data = MetaData {
information: information.to_string(),
from_symbol: from_symbol.to_string(),
to_symbol: to_symbol.to_string(),
last_refreshed: last_refreshed
.expect("last refreshed value contains None")
.to_string(),
interval: interval.map(ToString::to_string),
output_size: output_size_value.map(ToString::to_string),
time_zone: time_zone_value.to_string(),
};
let mut data_entries: Vec<Data> = Vec::new();
for hash in self.forex.unwrap().values() {
for val in hash.keys() {
let data_helper = hash
.get(val)
.expect("failed to get value from Forex hashmap");
data_entries.push(Data {
time: val.to_string(),
open: data_helper.open,
high: data_helper.high,
low: data_helper.low,
close: data_helper.close,
});
}
}
Ok(Forex {
data: data_entries,
meta_data,
})
}
}
impl FindData for Vec<Data> {
#[must_use]
fn find(&self, time: &str) -> Option<&<Self as IntoIterator>::Item> {
self.iter().find(|&data| data.time == time)
}
#[must_use]
fn latest(&self) -> <Self as IntoIterator>::Item {
let mut latest = &Data::default();
for data in self {
if latest.time < data.time {
latest = data;
}
}
latest.clone()
}
fn latest_n(&self, n: usize) -> Result<Vec<&<Self as IntoIterator>::Item>> {
let mut time_list = self.iter().map(|data| &data.time).collect::<Vec<_>>();
time_list.sort_by_key(|w| cmp::Reverse(*w));
if n > time_list.len() {
return Err(Error::DesiredNumberOfDataNotPresent(time_list.len()));
}
let mut full_list = Vec::<&Data>::new();
for time in &time_list[0..n] {
full_list.push(self.find(time).unwrap());
}
Ok(full_list)
}
}
pub struct ForexBuilder<'a> {
api_client: &'a ApiClient,
function: ForexFunction,
from_symbol: &'a str,
to_symbol: &'a str,
interval: Option<TimeSeriesInterval>,
output_size: Option<OutputSize>,
}
impl<'a> ForexBuilder<'a> {
crate::json_data_struct!(Forex, ForexHelper);
#[must_use]
pub fn new(
api_client: &'a ApiClient,
function: ForexFunction,
from_symbol: &'a str,
to_symbol: &'a str,
) -> Self {
Self {
api_client,
function,
from_symbol,
to_symbol,
interval: None,
output_size: None,
}
}
#[must_use]
pub fn interval(mut self, interval: TimeSeriesInterval) -> Self {
self.interval = Some(interval);
self
}
#[must_use]
pub fn output_size(mut self, output_size: OutputSize) -> Self {
self.output_size = Some(output_size);
self
}
fn create_url(&self) -> String {
let function = match self.function {
ForexFunction::IntraDay => "FX_INTRADAY",
ForexFunction::Daily => "FX_DAILY",
ForexFunction::Weekly => "FX_WEEKLY",
ForexFunction::Monthly => "FX_MONTHLY",
};
let mut url = format!(
"query?function={}&from_symbol={}&to_symbol={}",
function, self.from_symbol, self.to_symbol
);
if let Some(forex_interval) = &self.interval {
let interval = match forex_interval {
TimeSeriesInterval::OneMin => "1min",
TimeSeriesInterval::FiveMin => "5min",
TimeSeriesInterval::FifteenMin => "15min",
TimeSeriesInterval::ThirtyMin => "30min",
TimeSeriesInterval::SixtyMin => "60min",
};
url.push_str(&format!("&interval={interval}"));
};
if let Some(forex_output_size) = &self.output_size {
let size = match forex_output_size {
OutputSize::Full => "full",
OutputSize::Compact => "compact",
};
url.push_str(&format!("&outputsize={size}"));
}
url
}
}
#[derive(Clone)]
pub enum ForexFunction {
IntraDay,
Daily,
Weekly,
Monthly,
}