use chrono::NaiveDate;
use clap::{Parser, Subcommand, ValueEnum};
use hmrc_rates::{RateType, Rates, Updater, YearEnd, YearMonth};
use rust_decimal::Decimal;
#[derive(Parser)]
#[command(name = "hmrc-rates", version, about = "HMRC exchange rates")]
struct Cli {
#[arg(long, global = true)]
refresh: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Convert {
amount: String,
code: String,
date: NaiveDate,
},
Rate {
code: String,
period: YearMonth,
#[arg(long, value_enum, default_value_t = Series::Monthly)]
r#type: Series,
},
List {
#[arg(value_enum)]
r#type: Series,
},
Currencies {
#[arg(value_enum)]
r#type: Series,
},
}
#[derive(Copy, Clone, ValueEnum)]
enum Series {
Monthly,
Spot,
Average,
Weekly,
}
impl From<Series> for RateType {
fn from(series: Series) -> RateType {
match series {
Series::Monthly => RateType::Monthly,
Series::Spot => RateType::Spot,
Series::Average => RateType::Average,
Series::Weekly => RateType::Weekly,
}
}
}
fn year_end(year_month: YearMonth) -> Result<YearEnd, String> {
YearEnd::from_year_month(year_month)
.ok_or_else(|| format!("'{year_month}' is not a spot/average period (use MM = 03 or 12)"))
}
fn load(refresh: bool) -> Result<Rates, Box<dyn std::error::Error>> {
let updater = Updater::new();
Ok(if refresh {
updater.refreshed()?
} else {
updater.cached()
})
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let rates = load(cli.refresh)?;
match cli.command {
Command::Convert { amount, code, date } => {
let amount: Decimal = amount
.parse()
.map_err(|e| format!("invalid amount '{amount}': {e}"))?;
let rate = rates.monthly_rate(&code, date)?;
println!("{} ({})", rate.to_gbp(amount).round_dp(4), rate.period());
}
Command::Rate {
code,
period: year_month,
r#type,
} => {
let rate = match r#type {
Series::Monthly => rates.monthly_rate(&code, year_month)?,
Series::Spot => rates.spot(year_end(year_month)?)?.rate(&code)?,
Series::Average => rates.average(year_end(year_month)?)?.rate(&code)?,
Series::Weekly => {
return Err("use `convert` or `list weekly` for the weekly series".into());
}
};
println!(
"{} {} per £1 ({})",
rate.units_per_gbp(),
rate.currency(),
rate.period()
);
}
Command::List { r#type } => match r#type {
Series::Monthly => rates.months().for_each(|m| println!("{m}")),
Series::Spot => rates.spot_periods().for_each(|p| println!("{p}")),
Series::Average => rates.average_periods().for_each(|p| println!("{p}")),
Series::Weekly => rates.weeks().for_each(|w| println!("{w}")),
},
Command::Currencies { r#type } => {
rates
.currencies(r#type.into())
.for_each(|c| println!("{c}"));
}
}
Ok(())
}
fn main() {
if let Err(e) = run() {
eprintln!("error: {e}");
std::process::exit(1);
}
}