7 Commits

Author SHA1 Message Date
0f930271c7 Use smolstr since we only expect small strings 2025-12-17 22:10:46 +01:00
61deca69c6 Update dependencies, fix a spelling mistake, set license field and bump package-version 2025-12-17 11:46:17 +01:00
Love
292c9d4f34 Merge pull request #2 from lov3b/feature/lto
enable LTO for release-builds
2025-12-17 11:32:04 +01:00
0f921e978e enable LTO 2025-12-17 11:29:55 +01:00
3fce153c1c update for crates.io 2025-01-09 17:29:35 +01:00
416233afde For crates.io 2025-01-09 17:27:48 +01:00
f2f27a25b6 Instructions for dpkg 2025-01-09 17:16:12 +01:00
9 changed files with 590 additions and 459 deletions

921
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,38 @@
[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.0.0" version = "1.0.1"
edition = "2021" edition = "2021"
authors = ["Love Billenius <lovebillenius@disroot.org>"] authors = ["Love Billenius <lovebillenius@disroot.org>"]
license = "Zlib"
license-file = "LICENSE" license-file = "LICENSE"
keywords = [ keywords = [
"ECB", "ECB",
"European Central Bank",
"Bank", "Bank",
"Central Bank", "Central",
"exchange", "exchange",
"rates", "rates",
"eur",
"sek",
"usd",
"nok",
"gbp",
"pln",
"dkk",
"czk",
"isk",
"chf",
] ]
repository = "https://github.com/lov3b/ecb-rates"
rust-version = "1.83"
categories = ["finance", "command-line-utilities"]
[profile.release]
codegen-units = 1
lto = true
[[bin]] [[bin]]
name = "ecb-rates" name = "ecb-rates"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
anyhow = "1.0.95" anyhow = "1.0"
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.5.23", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
colored = "3.0.0" colored = "3.0"
quick-xml = { version = "0.37.2", features = ["async-tokio", "tokio"] } quick-xml = { version = "0.38", features = ["async-tokio", "tokio"] }
reqwest = "0.12.12" reqwest = "0.12"
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.134" serde_json = "1.0"
tokio = { version = "1.42.0", features = ["macros"] } smol_str = { version = "0.3", features = ["serde"] }
tokio = { version = "1.48", features = ["macros"] }

View File

@@ -7,7 +7,7 @@
alt="European Central Bank Logo" alt="European Central Bank Logo"
align="left" align="left"
/> />
A CLI utility to fetch exchange reates from the European Central Bank. A CLI utility to fetch exchange rates from the European Central Bank.
<br /> <br />
<br /> <br />
<br /> <br />
@@ -19,12 +19,18 @@
## Install ## Install
### Binary
If you're on Debian Linux, then just go over to the releases, and install the latest _.deb_ package with `dpkg`
### Source
First, make sure that you have the rust toolchain installed. If not, then go to [rustup](https://rustup.rs) to install it. First, make sure that you have the rust toolchain installed. If not, then go to [rustup](https://rustup.rs) to install it.
Now, run the following cargo command: Now, run the following cargo command:
```sh ```sh
cargo install --git https://github.com/lov3b/ecb-rates.git cargo install ecb-rates
``` ```
Congratulations! Now the cli binary `ecb-rates` will be in your cargo bin folder. Congratulations! Now the cli binary `ecb-rates` will be in your cargo bin folder.
@@ -37,7 +43,7 @@ It will fetch any of the following api nodes, and reduce them for you.
- Last available day. - Last available day.
- Last 90 days - Last 90 days
- Since the dawn of the *EUR* - Since the dawn of the _EUR_
#### Display select currencies #### Display select currencies
@@ -53,11 +59,11 @@ It features an extensive cache, which will [calculate hollidays](src/holiday.rs)
Change the rates for the perspective of any currency with the `--perspective` or `-p` flag. Change the rates for the perspective of any currency with the `--perspective` or `-p` flag.
Flip it from `EUR to ALL` to `ALL to EUR` with the `--invert` or `-i` flag. It will work as expected with the *perspective* option. Flip it from `EUR to ALL` to `ALL to EUR` with the `--invert` or `-i` flag. It will work as expected with the _perspective_ option.
#### Fast #### Fast
It wouldn't be a rust project without being *BLAZINGLY FAST*! When the cache is valid a single day will on my computer be shown in 3 ms. When the cache isn't being used it will be ~90ms. The cache speed will largely depend on your drive, the latter will depend on your network speed. Both options are fast enought to be in a `.bashrc` or `.zshrc` It wouldn't be a rust project without being _BLAZINGLY FAST_! When the cache is valid a single day will on my computer be shown in 3 ms. When the cache isn't being used it will be ~90ms. The cache speed will largely depend on your drive, the latter will depend on your network speed. Both options are fast enought to be in a `.bashrc` or `.zshrc`
### Examples ### Examples

View File

@@ -1,4 +1,5 @@
use clap::{arg, Parser, ValueEnum}; use clap::{arg, Parser, ValueEnum};
use smol_str::SmolStr;
use super::{ShowDays, SortBy}; use super::{ShowDays, SortBy};
@@ -8,7 +9,7 @@ use super::{ShowDays, SortBy};
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<String>, pub currencies: Vec<SmolStr>,
#[arg(value_enum, default_value_t = FormatOption::Plain)] #[arg(value_enum, default_value_t = FormatOption::Plain)]
pub command: FormatOption, pub command: FormatOption,
@@ -35,7 +36,7 @@ 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<String>, pub perspective: Option<SmolStr>,
/// Invert the rate /// Invert the rate
#[arg(long = "invert", short = 'i')] #[arg(long = "invert", short = 'i')]

View File

@@ -2,6 +2,7 @@ use clap::Parser as _;
use ecb_rates::cache::{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 ecb_rates::cli::{Cli, FormatOption}; use ecb_rates::cli::{Cli, FormatOption};
@@ -78,7 +79,7 @@ async fn main() -> ExitCode {
parsed parsed
}; };
cli.perspective = cli.perspective.map(|s| s.to_uppercase()); cli.perspective = cli.perspective.map(|s| s.to_uppercase_smolstr());
if let Some(currency) = cli.perspective.as_ref() { if let Some(currency) = cli.perspective.as_ref() {
header_description.replace_eur(&currency); header_description.replace_eur(&currency);
let error_occured = change_perspective(&mut parsed, &currency).is_none(); let error_occured = change_perspective(&mut parsed, &currency).is_none();
@@ -99,7 +100,7 @@ async fn main() -> ExitCode {
let currencies = cli let currencies = cli
.currencies .currencies
.iter() .iter()
.map(|x| x.to_uppercase()) .map(|x| x.to_uppercase_smolstr())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
filter_currencies(&mut parsed, &currencies); filter_currencies(&mut parsed, &currencies);

View File

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

View File

@@ -2,37 +2,44 @@ use std::collections::HashMap;
use quick_xml::events::Event; use quick_xml::events::Event;
use quick_xml::Reader; use quick_xml::Reader;
use smol_str::SmolStr;
use crate::models::ExchangeRateResult; use crate::models::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<String> = None; let mut current_time: Option<SmolStr> = 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<String>, current_time: &mut Option<SmolStr>,
inside_cube_time: &mut bool, inside_cube_time: &mut bool,
current_rates: &mut HashMap<String, f64>, current_rates: &mut HashMap<SmolStr, 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<String> = None; let mut time_attr: Option<SmolStr> = None;
let mut currency_attr: Option<String> = None; let mut currency_attr: Option<SmolStr> = None;
let mut rate_attr: Option<String> = None; let mut rate_attr: Option<SmolStr> = 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 = String::from_utf8_lossy(attr.value.as_ref()).to_string(); let val = smol_from_utf8(attr.value.as_ref());
match key { match key {
b"time" => { b"time" => {

View File

@@ -1,5 +1,7 @@
use std::fmt::Display; use std::fmt::Display;
use smol_str::SmolStr;
use crate::cli::SortBy; use crate::cli::SortBy;
use crate::models::ExchangeRateResult; use crate::models::ExchangeRateResult;
use crate::DEFAULT_WIDTH; use crate::DEFAULT_WIDTH;
@@ -8,20 +10,20 @@ 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<String>, pub(super) header: Option<SmolStr>,
pub(super) column_left: String, pub(super) column_left: SmolStr,
pub(super) column_right: String, pub(super) column_right: SmolStr,
pub(super) rows: Vec<(String, f64)>, pub(super) rows: Vec<(SmolStr, 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 = String; type Header = SmolStr;
type ColumnLeft = String; type ColumnLeft = SmolStr;
type ColumnRight = String; type ColumnRight = SmolStr;
type RowLeft = String; type RowLeft = SmolStr;
fn new( fn new(
header: Option<Self::Header>, header: Option<Self::Header>,
@@ -59,8 +61,8 @@ impl<'a> TableTrait<'a> for Table {
} }
impl TableGet for Table { impl TableGet for Table {
type RowLeftRef = String; type RowLeftRef = SmolStr;
type RowRightRef = String; type RowRightRef = SmolStr;
fn get_header(&self) -> Option<&str> { fn get_header(&self) -> Option<&str> {
self.header.as_deref() self.header.as_deref()
@@ -85,7 +87,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".to_string(), "Rate".to_string()); let mut table = Table::new(Some(value.time), "Currency".into(), "Rate".into());
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,10 +1,12 @@
use std::{borrow::BorrowMut, collections::HashMap, ops::Deref}; use std::{borrow::BorrowMut, collections::HashMap, ops::Deref};
use smol_str::SmolStr;
use crate::models::ExchangeRateResult; use crate::models::ExchangeRateResult;
pub fn filter_currencies(exchange_rate_results: &mut [ExchangeRateResult], currencies: &[String]) { pub fn filter_currencies(exchange_rate_results: &mut [ExchangeRateResult], currencies: &[SmolStr]) {
for exchange_rate in exchange_rate_results { for exchange_rate in exchange_rate_results {
let rates_ptr: *mut HashMap<String, f64> = &mut exchange_rate.rates; let rates_ptr: *mut HashMap<_, _> = &mut exchange_rate.rates;
exchange_rate exchange_rate
.rates .rates
.keys() .keys()
@@ -32,7 +34,7 @@ pub fn change_perspective(
*iter_rate = eur_rate * iter_rate.deref(); *iter_rate = eur_rate * iter_rate.deref();
} }
rate_res.rates.insert("EUR".to_string(), eur_rate); rate_res.rates.insert("EUR".into(), eur_rate);
} }
Some(()) Some(())
} }