diff --git a/src/cli.rs b/src/cli.rs index 0a047f7..ef92577 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -33,6 +33,18 @@ pub struct Cli { #[arg(value_enum, long = "sort-by", short = 's', default_value_t = SortBy::Currency)] pub sort_by: SortBy, + /// Recalculate to the perspective from an included currency + #[arg(long = "perspective", short = 'p')] + pub perspective: Option, + + /// Invert the rate + #[arg(long = "invert", short = 'i')] + pub should_invert: bool, + + //// Max decimals to keep in price. + #[arg(long = "max-decimals", short = 'd', default_value_t = 5)] + pub max_decimals: u8, + /// Amount of data #[arg(value_enum, default_value_t = Resolution::TODAY, long="resolution", short='r')] pub resolution: Resolution, diff --git a/src/header_description.rs b/src/header_description.rs new file mode 100644 index 0000000..6830cf3 --- /dev/null +++ b/src/header_description.rs @@ -0,0 +1,51 @@ +use colored::Colorize; +use std::fmt::Display; + +use crate::DEFAULT_WIDTH; + +pub struct HeaderDescription<'a> { + header_description: [&'a str; 2], +} + +impl<'a> HeaderDescription<'a> { + pub fn new() -> Self { + Self { + header_description: ["EUR", /*"\u{2217}"*/ "ALL"], // Unicode is ∗ + } + } + + pub fn invert(&mut self) { + self.header_description.swap(0, 1); + } + + pub fn replace_eur(&mut self, currency: &'a str) { + self.header_description[0] = currency; + } +} + +impl<'a> Display for HeaderDescription<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let width = DEFAULT_WIDTH - 2; + let formatted = format!( + "{} {} {}", + self.header_description[0].purple().bold(), + "to".italic(), + self.header_description[1].purple().bold() + ); + let unformatted_len = + self.header_description[0].len() + self.header_description[1].len() + 4; + let left_padding = " ".repeat((width - unformatted_len) / 2); + + let vertical = "═".repeat(width); + writeln!(f, " ╔{}╗", &vertical)?; + writeln!( + f, + " {}{}{} ", + &left_padding, + formatted, + " ".repeat(width - left_padding.len() - unformatted_len) + )?; + writeln!(f, " ╚{}╝\n", &vertical)?; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8c53513..3476748 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,18 @@ pub mod cache; pub mod cli; +mod header_description; mod holiday; pub mod models; pub mod os; pub mod parsing; pub mod table; +pub mod utils_calc; +pub use header_description::HeaderDescription; pub use holiday::Hollidays; const APP_NAME: &'static str = "ECB-rates"; +const DEFAULT_WIDTH: usize = 20; pub mod ecb_url { pub const TODAY: &'static str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"; diff --git a/src/main.rs b/src/main.rs index 4e128bf..38be7f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ use clap::Parser as _; use ecb_rates::cache::{Cache, CacheLine}; +use ecb_rates::HeaderDescription; use reqwest::{Client, IntoUrl}; -use std::{borrow::BorrowMut, collections::HashMap, process::ExitCode}; +use std::process::ExitCode; use ecb_rates::cli::{Cli, FormatOption}; use ecb_rates::models::ExchangeRateResult; use ecb_rates::parsing::parse; use ecb_rates::table::{TableRef, TableTrait as _}; +use ecb_rates::utils_calc::{change_perspective, filter_currencies, invert_rates, round}; async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result> { let client = Client::new(); @@ -14,31 +16,14 @@ async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result = &mut exchange_rate.rates; - exchange_rate - .rates - .keys() - .filter(|x| !currencies.contains(x)) - .for_each(|key_to_remove| { - /* This is safe, since we: - * 1. Already have a mutable reference. - * 2. Don't run the code in paralell - */ - let rates = unsafe { (*rates_ptr).borrow_mut() }; - rates.remove_entry(key_to_remove); - }); - } -} - #[tokio::main(flavor = "current_thread")] async fn main() -> ExitCode { - let cli = Cli::parse(); + let mut cli = Cli::parse(); if cli.force_color { colored::control::set_override(true); } + let mut header_description = HeaderDescription::new(); let use_cache = !cli.no_cache; let mut cache = if use_cache { Cache::load() } else { None }; let cache_ok = cache.as_ref().map_or_else( @@ -89,6 +74,23 @@ async fn main() -> ExitCode { parsed }; + cli.perspective = cli.perspective.map(|s| s.to_uppercase()); + if let Some(currency) = cli.perspective.as_ref() { + header_description.replace_eur(¤cy); + let error_occured = change_perspective(&mut parsed, ¤cy).is_none(); + if error_occured { + eprintln!("The currency wasn't in the data from the ECB!"); + return ExitCode::FAILURE; + } + } + + if cli.should_invert { + invert_rates(&mut parsed); + header_description.invert(); + } + + round(&mut parsed, cli.max_decimals); + if !cli.currencies.is_empty() { let currencies = cli .currencies @@ -122,18 +124,23 @@ async fn main() -> ExitCode { }; to_string_json(&json_values).expect("Failed to parse content as JSON") } - FormatOption::Plain => parsed - .iter() - .map(|x| { - let mut t: TableRef = x.into(); - if cli.no_time { - t.disable_header(); - } - t.sort(&cli.sort_by); - t.to_string() - }) - .collect::>() - .join("\n"), + FormatOption::Plain => { + let rates = parsed + .iter() + .map(|x| { + let mut t: TableRef = x.into(); + if cli.no_time { + t.disable_header(); + } + t.sort(&cli.sort_by); + t.to_string() + }) + .collect::>() + .join("\n"); + let mut s = header_description.to_string(); + s.push_str(&rates); + s + } }; println!("{}", &output); diff --git a/src/table/table_display.rs b/src/table/table_display.rs index 25821f6..855486a 100644 --- a/src/table/table_display.rs +++ b/src/table/table_display.rs @@ -7,6 +7,7 @@ pub fn helper_table_print( table: &T, ) -> std::fmt::Result { let width = table.get_width(); + let left_offset = " ".repeat(table.get_left_offset()); if let Some(header) = table.get_header() { let middle_padding_amount = (width - header.len()) / 2; @@ -14,7 +15,8 @@ pub fn helper_table_print( let middle_padding = " ".repeat(middle_padding_amount); writeln!( f, - "{}{}{}", + "{}{}{}{}", + &left_offset, middle_padding, header.bold().cyan(), middle_padding @@ -27,19 +29,27 @@ pub fn helper_table_print( let right_padding = " ".repeat(right_padding_amount); writeln!( f, - "{}{}{}", + "{}{}{}{}", + &left_offset, column_left.bold().yellow(), right_padding, column_right.bold().yellow() )?; - writeln!(f, "{}", "-".repeat(width))?; + writeln!(f, "{}{}", &left_offset, "-".repeat(width))?; for (left, right) in table.get_rows().iter() { let left_str = left.as_ref(); let right_str = right.to_string(); let padding_amount = width.saturating_sub(left_str.len() + right_str.len()); let padding = " ".repeat(padding_amount); - writeln!(f, "{}{}{}", left_str.bold().green(), padding, right_str)?; + writeln!( + f, + "{}{}{}{}", + &left_offset, + left_str.bold().green(), + padding, + right_str + )?; } Ok(()) diff --git a/src/table/table_getter.rs b/src/table/table_getter.rs index 4751932..56a069c 100644 --- a/src/table/table_getter.rs +++ b/src/table/table_getter.rs @@ -7,4 +7,5 @@ pub trait TableGet { fn get_column_right(&self) -> &str; fn get_rows(&self) -> &Vec<(Self::RowLeftRef, f64)>; fn get_width(&self) -> usize; + fn get_left_offset(&self) -> usize; } diff --git a/src/table/table_owned.rs b/src/table/table_owned.rs index 34aa372..f4f143a 100644 --- a/src/table/table_owned.rs +++ b/src/table/table_owned.rs @@ -2,6 +2,7 @@ use std::fmt::Display; use crate::cli::SortBy; use crate::models::ExchangeRateResult; +use crate::DEFAULT_WIDTH; use super::table_display::helper_table_print; use super::{TableGet, TableTrait}; @@ -13,6 +14,7 @@ pub struct Table { pub(super) rows: Vec<(String, f64)>, pub color: bool, pub width: usize, + pub left_offset: usize, } impl<'a> TableTrait<'a> for Table { @@ -32,7 +34,8 @@ impl<'a> TableTrait<'a> for Table { column_right, rows: Vec::new(), color: false, - width: 21, + width: DEFAULT_WIDTH, + left_offset: 1, } } @@ -74,6 +77,10 @@ impl TableGet for Table { fn get_width(&self) -> usize { self.width } + + fn get_left_offset(&self) -> usize { + self.left_offset + } } impl From for Table { diff --git a/src/table/table_ref.rs b/src/table/table_ref.rs index 122321c..b7b13ac 100644 --- a/src/table/table_ref.rs +++ b/src/table/table_ref.rs @@ -2,6 +2,7 @@ use std::fmt::Display; use crate::cli::SortBy; use crate::models::ExchangeRateResult; +use crate::DEFAULT_WIDTH; use super::table_display::helper_table_print; use super::table_getter::TableGet; @@ -15,6 +16,7 @@ pub struct TableRef<'a> { rows: Vec<(&'a str, f64)>, pub color: bool, pub width: usize, + pub left_offset: usize, } impl<'a> TableTrait<'a> for TableRef<'a> { @@ -34,7 +36,8 @@ impl<'a> TableTrait<'a> for TableRef<'a> { column_right, rows: Vec::new(), color: false, - width: 21, + width: DEFAULT_WIDTH, + left_offset: 1, } } @@ -75,6 +78,10 @@ impl<'a> TableGet for TableRef<'a> { fn get_width(&self) -> usize { self.width } + + fn get_left_offset(&self) -> usize { + self.left_offset + } } impl<'a> From<&'a ExchangeRateResult> for TableRef<'a> { @@ -109,6 +116,7 @@ impl<'a> From<&'a Table> for TableRef<'a> { rows, color: table.color, width: table.width, + left_offset: table.left_offset, } } } diff --git a/src/utils_calc.rs b/src/utils_calc.rs new file mode 100644 index 0000000..81b9e3e --- /dev/null +++ b/src/utils_calc.rs @@ -0,0 +1,56 @@ +use std::{borrow::BorrowMut, collections::HashMap, ops::Deref}; + +use crate::models::ExchangeRateResult; + +pub fn filter_currencies(exchange_rate_results: &mut [ExchangeRateResult], currencies: &[String]) { + for exchange_rate in exchange_rate_results { + let rates_ptr: *mut HashMap = &mut exchange_rate.rates; + exchange_rate + .rates + .keys() + .filter(|x| !currencies.contains(x)) + .for_each(|key_to_remove| { + /* This is safe, since we: + * 1. Already have a mutable reference. + * 2. Don't run the code in paralell + */ + let rates = unsafe { (*rates_ptr).borrow_mut() }; + rates.remove_entry(key_to_remove); + }); + } +} + +pub fn change_perspective( + exchange_rate_results: &mut [ExchangeRateResult], + currency: &str, +) -> Option<()> { + for rate_res in exchange_rate_results { + let currency_rate = rate_res.rates.remove(currency)?; + let eur_rate = 1.0 / currency_rate; + + for (_, iter_rate) in rate_res.rates.iter_mut() { + *iter_rate = eur_rate * iter_rate.deref(); + } + + rate_res.rates.insert("EUR".to_string(), eur_rate); + } + Some(()) +} + +pub fn invert_rates(exchange_rate_results: &mut [ExchangeRateResult]) { + for rate_res in exchange_rate_results { + for (_, iter_rate) in rate_res.rates.iter_mut() { + *iter_rate = 1.0 / *iter_rate; + } + } +} + +pub fn round(exchange_rate_results: &mut [ExchangeRateResult], max_decimals: u8) { + let power = 10.0_f64.powf(max_decimals as f64); + for rate_res in exchange_rate_results { + for (_, iter_rate) in rate_res.rates.iter_mut() { + let more = iter_rate.deref() * power; + *iter_rate = more.round() / power; + } + } +}