Merge branch 'perspective'

This commit is contained in:
Love 2025-01-09 13:57:45 +01:00
commit 5f3a075580
9 changed files with 194 additions and 38 deletions

View File

@ -33,6 +33,18 @@ pub struct Cli {
#[arg(value_enum, long = "sort-by", short = 's', default_value_t = SortBy::Currency)] #[arg(value_enum, long = "sort-by", short = 's', default_value_t = SortBy::Currency)]
pub sort_by: SortBy, pub sort_by: SortBy,
/// Recalculate to the perspective from an included currency
#[arg(long = "perspective", short = 'p')]
pub perspective: Option<String>,
/// 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 /// Amount of data
#[arg(value_enum, default_value_t = Resolution::TODAY, long="resolution", short='r')] #[arg(value_enum, default_value_t = Resolution::TODAY, long="resolution", short='r')]
pub resolution: Resolution, pub resolution: Resolution,

51
src/header_description.rs Normal file
View File

@ -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(())
}
}

View File

@ -1,14 +1,18 @@
pub mod cache; pub mod cache;
pub mod cli; pub mod cli;
mod header_description;
mod holiday; mod holiday;
pub mod models; pub mod models;
pub mod os; pub mod os;
pub mod parsing; pub mod parsing;
pub mod table; pub mod table;
pub mod utils_calc;
pub use header_description::HeaderDescription;
pub use holiday::Hollidays; pub use holiday::Hollidays;
const APP_NAME: &'static str = "ECB-rates"; const APP_NAME: &'static str = "ECB-rates";
const DEFAULT_WIDTH: usize = 20;
pub mod ecb_url { pub mod ecb_url {
pub const TODAY: &'static 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";

View File

@ -1,12 +1,14 @@
use clap::Parser as _; use clap::Parser as _;
use ecb_rates::cache::{Cache, CacheLine}; use ecb_rates::cache::{Cache, CacheLine};
use ecb_rates::HeaderDescription;
use reqwest::{Client, IntoUrl}; 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::cli::{Cli, FormatOption};
use ecb_rates::models::ExchangeRateResult; use ecb_rates::models::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};
async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result<Vec<ExchangeRateResult>> { async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result<Vec<ExchangeRateResult>> {
let client = Client::new(); let client = Client::new();
@ -14,31 +16,14 @@ async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result<Vec<ExchangeRateResu
parse(&xml_content) parse(&xml_content)
} }
fn filter_currencies(exchange_rate_results: &mut [ExchangeRateResult], currencies: &[String]) {
for exchange_rate in exchange_rate_results {
let rates_ptr: *mut HashMap<String, f64> = &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")] #[tokio::main(flavor = "current_thread")]
async fn main() -> ExitCode { async fn main() -> ExitCode {
let cli = Cli::parse(); let mut cli = Cli::parse();
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 use_cache = !cli.no_cache; let use_cache = !cli.no_cache;
let mut cache = if use_cache { Cache::load() } else { None }; let mut cache = if use_cache { Cache::load() } else { None };
let cache_ok = cache.as_ref().map_or_else( let cache_ok = cache.as_ref().map_or_else(
@ -89,6 +74,23 @@ async fn main() -> ExitCode {
parsed parsed
}; };
cli.perspective = cli.perspective.map(|s| s.to_uppercase());
if let Some(currency) = cli.perspective.as_ref() {
header_description.replace_eur(&currency);
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 {
invert_rates(&mut parsed);
header_description.invert();
}
round(&mut parsed, cli.max_decimals);
if !cli.currencies.is_empty() { if !cli.currencies.is_empty() {
let currencies = cli let currencies = cli
.currencies .currencies
@ -122,18 +124,23 @@ async fn main() -> ExitCode {
}; };
to_string_json(&json_values).expect("Failed to parse content as JSON") to_string_json(&json_values).expect("Failed to parse content as JSON")
} }
FormatOption::Plain => parsed FormatOption::Plain => {
.iter() let rates = parsed
.map(|x| { .iter()
let mut t: TableRef = x.into(); .map(|x| {
if cli.no_time { let mut t: TableRef = x.into();
t.disable_header(); if cli.no_time {
} t.disable_header();
t.sort(&cli.sort_by); }
t.to_string() t.sort(&cli.sort_by);
}) t.to_string()
.collect::<Vec<_>>() })
.join("\n"), .collect::<Vec<_>>()
.join("\n");
let mut s = header_description.to_string();
s.push_str(&rates);
s
}
}; };
println!("{}", &output); println!("{}", &output);

View File

@ -7,6 +7,7 @@ pub fn helper_table_print<T: TableGet>(
table: &T, table: &T,
) -> std::fmt::Result { ) -> std::fmt::Result {
let width = table.get_width(); let width = table.get_width();
let left_offset = " ".repeat(table.get_left_offset());
if let Some(header) = table.get_header() { if let Some(header) = table.get_header() {
let middle_padding_amount = (width - header.len()) / 2; let middle_padding_amount = (width - header.len()) / 2;
@ -14,7 +15,8 @@ pub fn helper_table_print<T: TableGet>(
let middle_padding = " ".repeat(middle_padding_amount); let middle_padding = " ".repeat(middle_padding_amount);
writeln!( writeln!(
f, f,
"{}{}{}", "{}{}{}{}",
&left_offset,
middle_padding, middle_padding,
header.bold().cyan(), header.bold().cyan(),
middle_padding middle_padding
@ -27,19 +29,27 @@ pub fn helper_table_print<T: TableGet>(
let right_padding = " ".repeat(right_padding_amount); let right_padding = " ".repeat(right_padding_amount);
writeln!( writeln!(
f, f,
"{}{}{}", "{}{}{}{}",
&left_offset,
column_left.bold().yellow(), column_left.bold().yellow(),
right_padding, right_padding,
column_right.bold().yellow() column_right.bold().yellow()
)?; )?;
writeln!(f, "{}", "-".repeat(width))?; writeln!(f, "{}{}", &left_offset, "-".repeat(width))?;
for (left, right) in table.get_rows().iter() { for (left, right) in table.get_rows().iter() {
let left_str = left.as_ref(); let left_str = left.as_ref();
let right_str = right.to_string(); let right_str = right.to_string();
let padding_amount = width.saturating_sub(left_str.len() + right_str.len()); let padding_amount = width.saturating_sub(left_str.len() + right_str.len());
let padding = " ".repeat(padding_amount); 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(()) Ok(())

View File

@ -7,4 +7,5 @@ pub trait TableGet {
fn get_column_right(&self) -> &str; fn get_column_right(&self) -> &str;
fn get_rows(&self) -> &Vec<(Self::RowLeftRef, f64)>; fn get_rows(&self) -> &Vec<(Self::RowLeftRef, f64)>;
fn get_width(&self) -> usize; fn get_width(&self) -> usize;
fn get_left_offset(&self) -> usize;
} }

View File

@ -2,6 +2,7 @@ use std::fmt::Display;
use crate::cli::SortBy; use crate::cli::SortBy;
use crate::models::ExchangeRateResult; use crate::models::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};
@ -13,6 +14,7 @@ pub struct Table {
pub(super) rows: Vec<(String, f64)>, pub(super) rows: Vec<(String, f64)>,
pub color: bool, pub color: bool,
pub width: usize, pub width: usize,
pub left_offset: usize,
} }
impl<'a> TableTrait<'a> for Table { impl<'a> TableTrait<'a> for Table {
@ -32,7 +34,8 @@ impl<'a> TableTrait<'a> for Table {
column_right, column_right,
rows: Vec::new(), rows: Vec::new(),
color: false, color: false,
width: 21, width: DEFAULT_WIDTH,
left_offset: 1,
} }
} }
@ -74,6 +77,10 @@ impl TableGet for Table {
fn get_width(&self) -> usize { fn get_width(&self) -> usize {
self.width self.width
} }
fn get_left_offset(&self) -> usize {
self.left_offset
}
} }
impl From<ExchangeRateResult> for Table { impl From<ExchangeRateResult> for Table {

View File

@ -2,6 +2,7 @@ use std::fmt::Display;
use crate::cli::SortBy; use crate::cli::SortBy;
use crate::models::ExchangeRateResult; use crate::models::ExchangeRateResult;
use crate::DEFAULT_WIDTH;
use super::table_display::helper_table_print; use super::table_display::helper_table_print;
use super::table_getter::TableGet; use super::table_getter::TableGet;
@ -15,6 +16,7 @@ pub struct TableRef<'a> {
rows: Vec<(&'a str, f64)>, rows: Vec<(&'a str, f64)>,
pub color: bool, pub color: bool,
pub width: usize, pub width: usize,
pub left_offset: usize,
} }
impl<'a> TableTrait<'a> for TableRef<'a> { impl<'a> TableTrait<'a> for TableRef<'a> {
@ -34,7 +36,8 @@ impl<'a> TableTrait<'a> for TableRef<'a> {
column_right, column_right,
rows: Vec::new(), rows: Vec::new(),
color: false, 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 { fn get_width(&self) -> usize {
self.width self.width
} }
fn get_left_offset(&self) -> usize {
self.left_offset
}
} }
impl<'a> From<&'a ExchangeRateResult> for TableRef<'a> { impl<'a> From<&'a ExchangeRateResult> for TableRef<'a> {
@ -109,6 +116,7 @@ impl<'a> From<&'a Table> for TableRef<'a> {
rows, rows,
color: table.color, color: table.color,
width: table.width, width: table.width,
left_offset: table.left_offset,
} }
} }
} }

56
src/utils_calc.rs Normal file
View File

@ -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<String, f64> = &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;
}
}
}