mirror of
https://github.com/lov3b/ecb-rates.git
synced 2025-02-22 18:00:11 +01:00
Merge branch 'perspective'
This commit is contained in:
commit
5f3a075580
12
src/cli.rs
12
src/cli.rs
@ -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
51
src/header_description.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -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";
|
||||||
|
71
src/main.rs
71
src/main.rs
@ -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(¤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() {
|
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);
|
||||||
|
@ -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(())
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
56
src/utils_calc.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user