From 0624499fcff44c201b39cc80e2b9530f806663e1 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Wed, 8 Jan 2025 19:42:06 +0100 Subject: [PATCH 1/7] Break out utils --- src/lib.rs | 1 + src/main.rs | 21 ++------------------- src/utils_calc.rs | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 src/utils_calc.rs diff --git a/src/lib.rs b/src/lib.rs index 8c53513..8c2f753 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod models; pub mod os; pub mod parsing; pub mod table; +pub mod utils_calc; pub use holiday::Hollidays; diff --git a/src/main.rs b/src/main.rs index 4e128bf..644ddfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ use clap::Parser as _; use ecb_rates::cache::{Cache, CacheLine}; 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::filter_currencies; async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result> { let client = Client::new(); @@ -14,24 +15,6 @@ 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(); diff --git a/src/utils_calc.rs b/src/utils_calc.rs new file mode 100644 index 0000000..f3a21cb --- /dev/null +++ b/src/utils_calc.rs @@ -0,0 +1,21 @@ +use std::{borrow::BorrowMut, collections::HashMap}; + +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); + }); + } +} From e20b7d22a5b8f11bd92b2dcd878a6ca41899c57c Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Wed, 8 Jan 2025 19:42:15 +0100 Subject: [PATCH 2/7] perspective option --- src/cli.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index 0a047f7..8d33e9e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -33,6 +33,10 @@ 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, + /// Amount of data #[arg(value_enum, default_value_t = Resolution::TODAY, long="resolution", short='r')] pub resolution: Resolution, From 19646e00014421d1e5db04fa6ebc29d639024463 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Wed, 8 Jan 2025 21:03:15 +0100 Subject: [PATCH 3/7] Perspective --- src/main.rs | 13 +++++++++++-- src/table/table_display.rs | 2 +- src/utils_calc.rs | 27 ++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 644ddfc..8737a5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ use clap::Parser as _; +use core::error; use ecb_rates::cache::{Cache, CacheLine}; -use reqwest::{Client, IntoUrl}; +use reqwest::{Client, IntoUrl, StatusCode}; 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::filter_currencies; +use ecb_rates::utils_calc::{change_perspective, filter_currencies}; async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result> { let client = Client::new(); @@ -72,6 +73,14 @@ async fn main() -> ExitCode { parsed }; + if let Some(currency) = cli.perspective { + 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.currencies.is_empty() { let currencies = cli .currencies diff --git a/src/table/table_display.rs b/src/table/table_display.rs index 25821f6..69a479f 100644 --- a/src/table/table_display.rs +++ b/src/table/table_display.rs @@ -36,7 +36,7 @@ pub fn helper_table_print( for (left, right) in table.get_rows().iter() { let left_str = left.as_ref(); - let right_str = right.to_string(); + let right_str = format!("{:.5}", right); 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)?; diff --git a/src/utils_calc.rs b/src/utils_calc.rs index f3a21cb..26e0695 100644 --- a/src/utils_calc.rs +++ b/src/utils_calc.rs @@ -1,4 +1,4 @@ -use std::{borrow::BorrowMut, collections::HashMap}; +use std::{borrow::BorrowMut, collections::HashMap, ops::Deref}; use crate::models::ExchangeRateResult; @@ -19,3 +19,28 @@ pub fn filter_currencies(exchange_rate_results: &mut [ExchangeRateResult], curre }); } } + +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; + } + } +} From 444568544418d022ce66ab1c6aa86a1a6cd7b2c9 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Thu, 9 Jan 2025 12:13:06 +0100 Subject: [PATCH 4/7] perspective to upper --- src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8737a5f..b177df5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ use clap::Parser as _; -use core::error; use ecb_rates::cache::{Cache, CacheLine}; -use reqwest::{Client, IntoUrl, StatusCode}; +use reqwest::{Client, IntoUrl}; use std::process::ExitCode; use ecb_rates::cli::{Cli, FormatOption}; @@ -73,7 +72,7 @@ async fn main() -> ExitCode { parsed }; - if let Some(currency) = cli.perspective { + if let Some(currency) = cli.perspective.map(|s| s.to_uppercase()) { let error_occured = change_perspective(&mut parsed, ¤cy).is_none(); if error_occured { eprintln!("The currency wasn't in the data from the ECB!"); From d842fb786cc9723560393f57e1d36a550e12c34e Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Thu, 9 Jan 2025 12:21:10 +0100 Subject: [PATCH 5/7] invert option --- src/cli.rs | 4 ++++ src/main.rs | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 8d33e9e..b2f4c0a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -37,6 +37,10 @@ pub struct Cli { #[arg(long = "perspective", short = 'p')] pub perspective: Option, + /// Invert the rate + #[arg(long = "invert", short = 'i')] + pub should_invert: bool, + /// Amount of data #[arg(value_enum, default_value_t = Resolution::TODAY, long="resolution", short='r')] pub resolution: Resolution, diff --git a/src/main.rs b/src/main.rs index b177df5..9e4d292 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ 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}; +use ecb_rates::utils_calc::{change_perspective, filter_currencies, invert_rates}; async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result> { let client = Client::new(); @@ -80,6 +80,10 @@ async fn main() -> ExitCode { } } + if cli.should_invert { + invert_rates(&mut parsed); + } + if !cli.currencies.is_empty() { let currencies = cli .currencies From 4b36904f3a09fbf90759bdef62d986832a997922 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Thu, 9 Jan 2025 12:33:00 +0100 Subject: [PATCH 6/7] Rounding --- src/cli.rs | 4 ++++ src/main.rs | 4 +++- src/table/table_display.rs | 2 +- src/utils_calc.rs | 10 ++++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index b2f4c0a..ef92577 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -41,6 +41,10 @@ pub struct Cli { #[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/main.rs b/src/main.rs index 9e4d292..f82d92a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ 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}; +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(); @@ -84,6 +84,8 @@ async fn main() -> ExitCode { invert_rates(&mut parsed); } + round(&mut parsed, cli.max_decimals); + if !cli.currencies.is_empty() { let currencies = cli .currencies diff --git a/src/table/table_display.rs b/src/table/table_display.rs index 69a479f..25821f6 100644 --- a/src/table/table_display.rs +++ b/src/table/table_display.rs @@ -36,7 +36,7 @@ pub fn helper_table_print( for (left, right) in table.get_rows().iter() { let left_str = left.as_ref(); - let right_str = format!("{:.5}", right); + 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)?; diff --git a/src/utils_calc.rs b/src/utils_calc.rs index 26e0695..81b9e3e 100644 --- a/src/utils_calc.rs +++ b/src/utils_calc.rs @@ -44,3 +44,13 @@ pub fn invert_rates(exchange_rate_results: &mut [ExchangeRateResult]) { } } } + +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; + } + } +} From af6f1682c5b0321e73a0c00177cec1dc5136cce6 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Thu, 9 Jan 2025 13:55:28 +0100 Subject: [PATCH 7/7] Header description --- src/header_description.rs | 51 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 +++ src/main.rs | 38 +++++++++++++++++----------- src/table/table_display.rs | 18 +++++++++++--- src/table/table_getter.rs | 1 + src/table/table_owned.rs | 9 ++++++- src/table/table_ref.rs | 10 +++++++- 7 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 src/header_description.rs 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 8c2f753..3476748 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod cache; pub mod cli; +mod header_description; mod holiday; pub mod models; pub mod os; @@ -7,9 +8,11 @@ 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 f82d92a..38be7f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use clap::Parser as _; use ecb_rates::cache::{Cache, CacheLine}; +use ecb_rates::HeaderDescription; use reqwest::{Client, IntoUrl}; use std::process::ExitCode; @@ -17,11 +18,12 @@ async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result 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( @@ -72,7 +74,9 @@ async fn main() -> ExitCode { parsed }; - if let Some(currency) = cli.perspective.map(|s| s.to_uppercase()) { + 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!"); @@ -82,6 +86,7 @@ async fn main() -> ExitCode { if cli.should_invert { invert_rates(&mut parsed); + header_description.invert(); } round(&mut parsed, cli.max_decimals); @@ -119,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, } } }