1 Commits

19 changed files with 254 additions and 159 deletions

44
Cargo.lock generated
View File

@@ -91,15 +91,6 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "borsh"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f"
dependencies = [
"cfg_aliases",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.1" version = "3.19.1"
@@ -128,12 +119,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.42" version = "0.4.42"
@@ -232,7 +217,7 @@ dependencies = [
[[package]] [[package]]
name = "ecb-rates" name = "ecb-rates"
version = "1.1.0" version = "1.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -242,7 +227,6 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"smol_str",
"tokio", "tokio",
] ]
@@ -977,9 +961,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.13.2" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
dependencies = [ dependencies = [
"zeroize", "zeroize",
] ]
@@ -1112,16 +1096,6 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smol_str"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5"
dependencies = [
"borsh",
"serde_core",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.1" version = "0.6.1"
@@ -1236,9 +1210,21 @@ dependencies = [
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tokio-native-tls" name = "tokio-native-tls"
version = "0.3.1" version = "0.3.1"

View File

@@ -1,10 +1,11 @@
[package] [package]
name = "ecb-rates" name = "ecb-rates"
description = "Query exchange rates from the European Central Bank (ECB)" description = "Query exchange rates from the European Central Bank (ECB)"
version = "1.1.0" version = "1.0.1"
edition = "2024" edition = "2021"
authors = ["Love Billenius <lovebillenius@disroot.org>"] authors = ["Love Billenius <lovebillenius@disroot.org>"]
license = "Zlib" license = "Zlib"
license-file = "LICENSE"
keywords = [ keywords = [
"ECB", "ECB",
"Bank", "Bank",
@@ -13,7 +14,7 @@ keywords = [
"rates", "rates",
] ]
repository = "https://github.com/lov3b/ecb-rates" repository = "https://github.com/lov3b/ecb-rates"
rust-version = "1.92" rust-version = "1.83"
categories = ["finance", "command-line-utilities"] categories = ["finance", "command-line-utilities"]
[profile.release] [profile.release]
@@ -33,5 +34,4 @@ quick-xml = { version = "0.38", features = ["async-tokio", "tokio"] }
reqwest = "0.12" reqwest = "0.12"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
smol_str = { version = "0.3", features = ["serde"] } tokio = { version = "1.48", features = ["macros"] }
tokio = "1.48"

View File

@@ -3,8 +3,8 @@ use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::CacheLine; use super::CacheLine;
use crate::View;
use crate::os::Os; use crate::os::Os;
use crate::View;
#[derive(Debug)] #[derive(Debug)]
pub struct Cache { pub struct Cache {

View File

@@ -4,8 +4,8 @@ use chrono::serde::ts_seconds;
use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, TimeDelta, Utc, Weekday}; use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, TimeDelta, Utc, Weekday};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::Hollidays;
use crate::models::ExchangeRateResult; use crate::models::ExchangeRateResult;
use crate::Hollidays;
const CET: FixedOffset = unsafe { FixedOffset::east_opt(3600).unwrap_unchecked() }; const CET: FixedOffset = unsafe { FixedOffset::east_opt(3600).unwrap_unchecked() };

View File

@@ -1,14 +1,14 @@
use clap::{Parser, ValueEnum}; use clap::{arg, Parser, ValueEnum};
use smol_str::SmolStr;
use super::{ShowDays, SortBy}; use super::{ShowDays, SortBy};
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(author, version, about)] #[command(author, version, about)]
pub struct Cli { pub struct Cli {
/// Which currencies do you want to fetch rates for? /// Which currencies do you want to fetch rates for?
#[arg(long = "currencies", short = 'c')] #[arg(long = "currencies", short = 'c')]
pub currencies: Vec<SmolStr>, pub currencies: Vec<String>,
#[arg(value_enum, default_value_t = FormatOption::Plain)] #[arg(value_enum, default_value_t = FormatOption::Plain)]
pub command: FormatOption, pub command: FormatOption,
@@ -35,13 +35,13 @@ pub struct Cli {
/// Recalculate to the perspective from an included currency /// Recalculate to the perspective from an included currency
#[arg(long = "perspective", short = 'p')] #[arg(long = "perspective", short = 'p')]
pub perspective: Option<SmolStr>, pub perspective: Option<String>,
/// Invert the rate /// Invert the rate
#[arg(long = "invert", short = 'i')] #[arg(long = "invert", short = 'i')]
pub should_invert: bool, pub should_invert: bool,
/// Max decimals to keep in price. //// Max decimals to keep in price.
#[arg(long = "max-decimals", short = 'd', default_value_t = 5)] #[arg(long = "max-decimals", short = 'd', default_value_t = 5)]
pub max_decimals: u8, pub max_decimals: u8,

View File

@@ -1,5 +1,7 @@
use clap::ValueEnum; use clap::ValueEnum;
use crate::models::Currency;
#[derive(Debug, ValueEnum, Clone)] #[derive(Debug, ValueEnum, Clone)]
pub enum SortBy { pub enum SortBy {
Currency, Currency,
@@ -7,9 +9,9 @@ pub enum SortBy {
} }
impl SortBy { impl SortBy {
pub fn get_comparer(&self) -> fn(&(&str, f64), &(&str, f64)) -> std::cmp::Ordering { pub fn get_comparer(&self) -> fn(&(&Currency, f64), &(&Currency, f64)) -> std::cmp::Ordering {
match self { match self {
Self::Currency => |a, b| a.0.cmp(b.0), Self::Currency => |a, b| a.0.as_ref().cmp(b.0.as_ref()),
Self::Rate => |a, b| a.1.total_cmp(&b.1), Self::Rate => |a, b| a.1.total_cmp(&b.1),
} }
} }

View File

@@ -7,12 +7,6 @@ pub struct HeaderDescription<'a> {
header_description: [&'a str; 2], header_description: [&'a str; 2],
} }
impl<'a> Default for HeaderDescription<'a> {
fn default() -> Self {
Self::new()
}
}
impl<'a> HeaderDescription<'a> { impl<'a> HeaderDescription<'a> {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View File

@@ -134,14 +134,14 @@ mod tests {
#[should_panic] #[should_panic]
fn test_year_too_low() { fn test_year_too_low() {
disable_panic_stack_trace(); disable_panic_stack_trace();
let _ = Hollidays::new(1000); let _ = Hollidays::new(1000);
} }
#[test] #[test]
#[should_panic] #[should_panic]
fn test_year_too_high() { fn test_year_too_high() {
disable_panic_stack_trace(); disable_panic_stack_trace();
let _ = Hollidays::new(9999); let _ = Hollidays::new(9999);
} }
fn disable_panic_stack_trace() { fn disable_panic_stack_trace() {

View File

@@ -1,4 +1,4 @@
pub mod caching; pub mod cache;
pub mod cli; pub mod cli;
mod header_description; mod header_description;
mod holiday; mod holiday;
@@ -13,15 +13,16 @@ pub use header_description::HeaderDescription;
pub use holiday::Hollidays; pub use holiday::Hollidays;
pub use view::View; pub use view::View;
const APP_NAME: &str = "ECB-rates"; const APP_NAME: &'static str = "ECB-rates";
const DEFAULT_WIDTH: usize = 20; const DEFAULT_WIDTH: usize = 20;
pub mod ecb_url { pub mod ecb_url {
pub const TODAY: &str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"; pub const TODAY: &'static str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml";
pub mod hist { pub mod hist {
pub const DAYS_ALL: &str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml"; pub const DAYS_ALL: &'static str =
pub const DAYS_90: &str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml";
pub const DAYS_90: &'static str =
"https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml"; "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml";
} }
} }

View File

@@ -1,13 +1,12 @@
use anyhow::Context;
use clap::Parser as _; use clap::Parser as _;
use ecb_rates::caching::{Cache, CacheLine}; use ecb_rates::cache::{Cache, CacheLine};
use ecb_rates::HeaderDescription; use ecb_rates::HeaderDescription;
use reqwest::{Client, IntoUrl}; use reqwest::{Client, IntoUrl};
use smol_str::StrExt;
use std::process::ExitCode; use std::process::ExitCode;
use std::str::FromStr;
use ecb_rates::cli::{Cli, FormatOption}; use ecb_rates::cli::{Cli, FormatOption};
use ecb_rates::models::ExchangeRateResult; use ecb_rates::models::{Currency, ExchangeRateResult};
use ecb_rates::parsing::parse; use ecb_rates::parsing::parse;
use ecb_rates::table::{TableRef, TableTrait as _}; use ecb_rates::table::{TableRef, TableTrait as _};
use ecb_rates::utils_calc::{change_perspective, filter_currencies, invert_rates, round}; use ecb_rates::utils_calc::{change_perspective, filter_currencies, invert_rates, round};
@@ -18,40 +17,22 @@ async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result<Vec<ExchangeRateResu
parse(&xml_content) parse(&xml_content)
} }
fn main() -> ExitCode { #[tokio::main(flavor = "current_thread")]
let cli = Cli::parse(); async fn main() -> ExitCode {
let mut cli = Cli::parse();
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(e) => {
eprintln!("Failed to initialize asynchronous runtime: {:?}", e);
return ExitCode::FAILURE;
}
};
match runtime.block_on(async_main(cli)) {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("Fatal: {:?}", e);
ExitCode::FAILURE
}
}
}
async fn async_main(mut cli: Cli) -> anyhow::Result<()> {
if cli.force_color { if cli.force_color {
colored::control::set_override(true); colored::control::set_override(true);
} }
let mut header_description = HeaderDescription::new(); let mut header_description = HeaderDescription::new();
let use_cache = !cli.no_cache; let use_cache = !cli.no_cache;
let view = cli let view = match cli.show_days.to_view() {
.show_days Some(v) => v,
.to_view() None => {
.context("It doesn't make any sence to fetch 0 days right?")?; eprintln!("It doesn't make any sence to fetch 0 days right?");
return ExitCode::SUCCESS;
}
};
let mut cache = if use_cache { Cache::load(&view) } else { None }; let mut cache = if use_cache { Cache::load(&view) } else { None };
let cache_ok = cache.as_ref().map_or_else( let cache_ok = cache.as_ref().map_or_else(
|| false, || false,
@@ -67,9 +48,14 @@ async fn async_main(mut cli: Cli) -> anyhow::Result<()> {
.exchange_rate_results .exchange_rate_results
.clone() .clone()
} else { } else {
let parsed = get_and_parse(view.to_ecb_url()) let parsed = match get_and_parse(view.to_ecb_url()).await {
.await Ok(k) => k,
.context("Failed to get/parse data from ECB")?; Err(e) => {
eprintln!("Failed to get/parse data from ECB: {}", e);
return ExitCode::FAILURE;
}
};
if !cache_ok { if !cache_ok {
let not_equal_cache = cache.as_ref().map_or_else( let not_equal_cache = cache.as_ref().map_or_else(
|| true, || true,
@@ -80,20 +66,37 @@ async fn async_main(mut cli: Cli) -> anyhow::Result<()> {
}, },
); );
if not_equal_cache && let Some(cache_safe) = cache.as_mut() { if not_equal_cache {
let cache_line = CacheLine::new(parsed.clone()); if let Some(cache_safe) = cache.as_mut() {
cache_safe.set_cache_line(cache_line); let cache_line = CacheLine::new(parsed.clone());
cache_safe.save()?; cache_safe.set_cache_line(cache_line);
if let Err(e) = cache_safe.save() {
eprintln!("Failed to save to cache with: {:?}", e);
}
}
} }
} }
parsed parsed
}; };
cli.perspective = cli.perspective.map(|s| s.to_uppercase_smolstr()); cli.perspective = cli.perspective.map(|s| s.to_uppercase());
if let Some(currency) = cli.perspective.as_ref() { let parsed_currency = match cli.perspective.as_ref() {
header_description.replace_eur(currency); Some(currency) => match Currency::from_str(currency) {
change_perspective(&mut parsed, currency) Ok(k) => Some(k),
.context("The currency wasn't in the data from the ECB!")?; Err(e) => {
eprintln!("The currency code '{}' is invalid: {:?}", currency, e);
return ExitCode::FAILURE;
}
},
None => None,
};
if let Some(currency) = parsed_currency.as_ref() {
header_description.replace_eur(currency.as_ref());
let error_occured = change_perspective(&mut parsed, &currency).is_none();
if error_occured {
eprintln!("The currency wasn't in the data from the ECB!");
return ExitCode::FAILURE;
}
} }
if cli.should_invert { if cli.should_invert {
@@ -104,11 +107,19 @@ async fn async_main(mut cli: Cli) -> anyhow::Result<()> {
round(&mut parsed, cli.max_decimals); round(&mut parsed, cli.max_decimals);
if !cli.currencies.is_empty() { if !cli.currencies.is_empty() {
let currencies = cli let currencies = match cli
.currencies .currencies
.iter() .iter()
.map(|x| x.to_uppercase_smolstr()) .map(|x| x.to_uppercase())
.collect::<Vec<_>>(); .map(|x| Currency::from_str(&x))
.collect::<anyhow::Result<Vec<_>>>()
{
Ok(k) => k,
Err(e) => {
eprintln!("Failed to parse currenc(y/ies): {:?}", e);
return ExitCode::FAILURE;
}
};
filter_currencies(&mut parsed, &currencies); filter_currencies(&mut parsed, &currencies);
} }
@@ -168,5 +179,5 @@ async fn async_main(mut cli: Cli) -> anyhow::Result<()> {
}; };
println!("{}", &output); println!("{}", &output);
Ok(()) ExitCode::SUCCESS
} }

97
src/models/currency.rs Normal file
View File

@@ -0,0 +1,97 @@
use std::{
fmt,
ops::Index,
slice::Iter,
str::{self, FromStr},
};
use serde::{de, Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Currency {
name: [u8; 3],
}
impl Currency {
pub fn as_str(&self) -> &str {
// SAFETY: We validate that bytes are ASCII in FromStr.
unsafe { str::from_utf8_unchecked(&self.name) }
}
pub fn iter(&self) -> Iter<'_, u8> {
self.name.iter()
}
}
impl AsRef<str> for Currency {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Serialize for Currency {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Currency {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(de::Error::custom)
}
}
impl FromStr for Currency {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != 3 {
anyhow::bail!("Currency code must be exactly 3 chars");
}
if !s.is_ascii() {
anyhow::bail!("Currency code must be ASCII");
}
let b = s.as_bytes();
Ok(Self {
name: [b[0], b[1], b[2]],
})
}
}
impl TryFrom<&str> for Currency {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Currency::from_str(value)
}
}
impl fmt::Display for Currency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl Index<usize> for Currency {
type Output = u8;
fn index(&self, index: usize) -> &Self::Output {
&self.name[index]
}
}
impl<'a> IntoIterator for &'a Currency {
type Item = &'a u8;
type IntoIter = Iter<'a, u8>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}

View File

@@ -1,9 +1,10 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::collections::HashMap; use std::collections::HashMap;
use super::Currency;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ExchangeRateResult { pub struct ExchangeRateResult {
pub time: SmolStr, pub time: String,
pub rates: HashMap<SmolStr, f64>, pub rates: HashMap<Currency, f64>,
} }

5
src/models/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
mod currency;
mod exchange_rate_result;
pub use currency::Currency;
pub use exchange_rate_result::ExchangeRateResult;

View File

@@ -1,52 +1,45 @@
use std::collections::HashMap; use std::{collections::HashMap, str::FromStr};
use quick_xml::Reader;
use quick_xml::events::Event; use quick_xml::events::Event;
use smol_str::SmolStr; use quick_xml::Reader;
use crate::models::ExchangeRateResult; use crate::models::{Currency, ExchangeRateResult};
fn smol_from_utf8(bytes: &[u8]) -> SmolStr {
str::from_utf8(bytes)
.map(SmolStr::new)
.unwrap_or_else(|_| SmolStr::new(String::from_utf8_lossy(bytes)))
}
pub fn parse(xml: &str) -> anyhow::Result<Vec<ExchangeRateResult>> { pub fn parse(xml: &str) -> anyhow::Result<Vec<ExchangeRateResult>> {
let mut reader = Reader::from_str(xml); let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true); reader.config_mut().trim_text(true);
let mut results = Vec::new(); let mut results = Vec::new();
let mut current_time: Option<SmolStr> = None; let mut current_time: Option<String> = None;
let mut inside_cube_time = false; let mut inside_cube_time = false;
let mut current_rates = HashMap::new(); let mut current_rates = HashMap::new();
fn handle_cube_element( fn handle_cube_element(
e: &quick_xml::events::BytesStart, e: &quick_xml::events::BytesStart,
current_time: &mut Option<SmolStr>, current_time: &mut Option<String>,
inside_cube_time: &mut bool, inside_cube_time: &mut bool,
current_rates: &mut HashMap<SmolStr, f64>, current_rates: &mut HashMap<Currency, f64>,
results: &mut Vec<ExchangeRateResult>, results: &mut Vec<ExchangeRateResult>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if e.name().local_name().as_ref() != b"Cube" { if e.name().local_name().as_ref() != b"Cube" {
return Ok(()); return Ok(());
} }
let mut time_attr: Option<SmolStr> = None; let mut time_attr: Option<String> = None;
let mut currency_attr: Option<SmolStr> = None; let mut currency_attr: Option<Currency> = None;
let mut rate_attr: Option<SmolStr> = None; let mut rate_attr: Option<String> = None;
for attr_result in e.attributes() { for attr_result in e.attributes() {
let attr = attr_result?; let attr = attr_result?;
let key = attr.key.as_ref(); let key = attr.key.as_ref();
let val = smol_from_utf8(attr.value.as_ref()); let val = String::from_utf8_lossy(attr.value.as_ref()).to_string();
match key { match key {
b"time" => { b"time" => {
time_attr = Some(val); time_attr = Some(val);
} }
b"currency" => { b"currency" => {
currency_attr = Some(val); currency_attr = Some(Currency::from_str(&val)?);
} }
b"rate" => { b"rate" => {
rate_attr = Some(val); rate_attr = Some(val);
@@ -68,9 +61,11 @@ pub fn parse(xml: &str) -> anyhow::Result<Vec<ExchangeRateResult>> {
*inside_cube_time = true; *inside_cube_time = true;
} }
if *inside_cube_time && let (Some(c), Some(r_str)) = (currency_attr, rate_attr) { if *inside_cube_time {
let r = r_str.parse::<f64>()?; if let (Some(c), Some(r_str)) = (currency_attr, rate_attr) {
current_rates.insert(c, r); let r = r_str.parse::<f64>()?;
current_rates.insert(c, r);
}
} }
Ok(()) Ok(())

View File

@@ -1,10 +1,11 @@
mod table_display;
mod table_getter; mod table_getter;
mod table_owned; mod table_owned;
mod table_ref; mod table_ref;
mod table_trait; mod table_trait;
mod table_display;
pub use table_getter::TableGet; pub use table_getter::TableGet;
pub use table_owned::Table; pub use table_owned::Table;
pub use table_ref::TableRef; pub use table_ref::TableRef;
pub use table_trait::TableTrait; pub use table_trait::TableTrait;

View File

@@ -1,29 +1,27 @@
use std::fmt::Display; use std::fmt::Display;
use smol_str::SmolStr;
use crate::DEFAULT_WIDTH;
use crate::cli::SortBy; use crate::cli::SortBy;
use crate::models::ExchangeRateResult; use crate::models::{Currency, ExchangeRateResult};
use crate::DEFAULT_WIDTH;
use super::table_display::helper_table_print; use super::table_display::helper_table_print;
use super::{TableGet, TableTrait}; use super::{TableGet, TableTrait};
pub struct Table { pub struct Table {
pub(super) header: Option<SmolStr>, pub(super) header: Option<String>,
pub(super) column_left: SmolStr, pub(super) column_left: String,
pub(super) column_right: SmolStr, pub(super) column_right: String,
pub(super) rows: Vec<(SmolStr, f64)>, pub(super) rows: Vec<(Currency, f64)>,
pub color: bool, pub color: bool,
pub width: usize, pub width: usize,
pub left_offset: usize, pub left_offset: usize,
} }
impl<'a> TableTrait<'a> for Table { impl<'a> TableTrait<'a> for Table {
type Header = SmolStr; type Header = String;
type ColumnLeft = SmolStr; type ColumnLeft = String;
type ColumnRight = SmolStr; type ColumnRight = String;
type RowLeft = SmolStr; type RowLeft = Currency;
fn new( fn new(
header: Option<Self::Header>, header: Option<Self::Header>,
@@ -61,8 +59,8 @@ impl<'a> TableTrait<'a> for Table {
} }
impl TableGet for Table { impl TableGet for Table {
type RowLeftRef = SmolStr; type RowLeftRef = Currency;
type RowRightRef = SmolStr; type RowRightRef = String;
fn get_header(&self) -> Option<&str> { fn get_header(&self) -> Option<&str> {
self.header.as_deref() self.header.as_deref()
@@ -87,7 +85,7 @@ impl TableGet for Table {
impl From<ExchangeRateResult> for Table { impl From<ExchangeRateResult> for Table {
fn from(value: ExchangeRateResult) -> Self { fn from(value: ExchangeRateResult) -> Self {
let mut table = Table::new(Some(value.time), "Currency".into(), "Rate".into()); let mut table = Table::new(Some(value.time), "Currency".to_string(), "Rate".to_string());
for (key, val) in value.rates.into_iter() { for (key, val) in value.rates.into_iter() {
table.add_row(key, val); table.add_row(key, val);
} }

View File

@@ -1,19 +1,19 @@
use std::fmt::Display; use std::fmt::Display;
use crate::DEFAULT_WIDTH;
use crate::cli::SortBy; use crate::cli::SortBy;
use crate::models::ExchangeRateResult; use crate::models::{Currency, ExchangeRateResult};
use crate::DEFAULT_WIDTH;
use super::Table;
use super::table_display::helper_table_print; use super::table_display::helper_table_print;
use super::table_getter::TableGet; use super::table_getter::TableGet;
use super::table_trait::TableTrait; use super::table_trait::TableTrait;
use super::Table;
pub struct TableRef<'a> { pub struct TableRef<'a> {
header: Option<&'a str>, header: Option<&'a str>,
column_left: &'a str, column_left: &'a str,
column_right: &'a str, column_right: &'a str,
rows: Vec<(&'a str, f64)>, rows: Vec<(&'a Currency, f64)>,
pub color: bool, pub color: bool,
pub width: usize, pub width: usize,
pub left_offset: usize, pub left_offset: usize,
@@ -23,7 +23,7 @@ impl<'a> TableTrait<'a> for TableRef<'a> {
type Header = &'a str; type Header = &'a str;
type ColumnLeft = &'a str; type ColumnLeft = &'a str;
type ColumnRight = &'a str; type ColumnRight = &'a str;
type RowLeft = &'a str; type RowLeft = &'a Currency;
fn new( fn new(
header: Option<Self::Header>, header: Option<Self::Header>,
@@ -60,7 +60,7 @@ impl<'a> TableTrait<'a> for TableRef<'a> {
} }
impl<'a> TableGet for TableRef<'a> { impl<'a> TableGet for TableRef<'a> {
type RowLeftRef = &'a str; type RowLeftRef = &'a Currency;
type RowRightRef = &'a str; type RowRightRef = &'a str;
fn get_header(&self) -> Option<&str> { fn get_header(&self) -> Option<&str> {
@@ -106,7 +106,7 @@ impl<'a> From<&'a Table> for TableRef<'a> {
let rows = table let rows = table
.rows .rows
.iter() .iter()
.map(|(left, right)| (left.as_str(), *right)) .map(|(left, right)| (left, *right))
.collect(); .collect();
TableRef { TableRef {

View File

@@ -1,12 +1,13 @@
use std::{borrow::BorrowMut, collections::HashMap, ops::Deref}; use std::{borrow::BorrowMut, collections::HashMap, ops::Deref, str::FromStr};
use smol_str::SmolStr; use crate::models::{Currency, ExchangeRateResult};
use crate::models::ExchangeRateResult; pub fn filter_currencies(
exchange_rate_results: &mut [ExchangeRateResult],
pub fn filter_currencies(exchange_rate_results: &mut [ExchangeRateResult], currencies: &[SmolStr]) { currencies: &[Currency],
) {
for exchange_rate in exchange_rate_results { for exchange_rate in exchange_rate_results {
let rates_ptr: *mut HashMap<_, _> = &mut exchange_rate.rates; let rates_ptr: *mut HashMap<Currency, f64> = &mut exchange_rate.rates;
exchange_rate exchange_rate
.rates .rates
.keys() .keys()
@@ -24,7 +25,7 @@ pub fn filter_currencies(exchange_rate_results: &mut [ExchangeRateResult], curre
pub fn change_perspective( pub fn change_perspective(
exchange_rate_results: &mut [ExchangeRateResult], exchange_rate_results: &mut [ExchangeRateResult],
currency: &str, currency: &Currency,
) -> Option<()> { ) -> Option<()> {
for rate_res in exchange_rate_results { for rate_res in exchange_rate_results {
let currency_rate = rate_res.rates.remove(currency)?; let currency_rate = rate_res.rates.remove(currency)?;
@@ -34,7 +35,10 @@ pub fn change_perspective(
*iter_rate = eur_rate * iter_rate.deref(); *iter_rate = eur_rate * iter_rate.deref();
} }
rate_res.rates.insert("EUR".into(), eur_rate); rate_res.rates.insert(
unsafe { Currency::from_str("EUR").unwrap_unchecked() },
eur_rate,
);
} }
Some(()) Some(())
} }