use std::path::PathBuf;
use clap::Args;
use retiretui_engine::project::{Projection, Summary, YearRow, project};
use serde::Serialize;
use super::project::OutputFormat;
use retiretui_tui::metric::Metric;
use retiretui_tui::table::{align, display_dollars, summary_table};
#[derive(Args)]
pub struct CompareArgs {
#[arg(num_args = 2..)]
pub plans: Vec<PathBuf>,
#[arg(long, value_enum)]
pub metric: Option<Metric>,
#[arg(long, value_enum, default_value_t = OutputFormat::Table)]
pub format: OutputFormat,
#[arg(long)]
pub nominal: bool,
#[arg(long)]
pub tax_dir: Vec<PathBuf>,
}
struct ComparedPlan {
path: PathBuf,
name: Option<String>,
projection: Projection,
}
impl ComparedPlan {
fn label(&self) -> String {
self.name.clone().unwrap_or_else(|| {
self.path.file_stem().map_or_else(
|| self.path.display().to_string(),
|stem| stem.to_string_lossy().into_owned(),
)
})
}
}
pub fn run(args: &CompareArgs) -> anyhow::Result<()> {
let tables = super::load_tables(&args.tax_dir)?;
let mut compared = Vec::with_capacity(args.plans.len());
for path in &args.plans {
let plan = super::load_validated_plan(path, &tables)?;
compared.push(ComparedPlan {
projection: project(&plan, &tables),
name: plan.plan.name,
path: path.clone(),
});
}
match args.format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&reply(&compared, args))?);
}
OutputFormat::Table => match args.metric {
Some(metric) => print!("{}", metric_table(&compared, metric, args.nominal)),
None => print!("{}", compared_table(&compared, args.nominal)),
},
}
Ok(())
}
fn compared_table(compared: &[ComparedPlan], nominal: bool) -> String {
let rows = compared
.iter()
.map(|plan| (vec![plan.label()], plan.projection.summary(!nominal)))
.collect();
summary_table(&["plan"], rows)
}
fn metric_table(compared: &[ComparedPlan], metric: Metric, nominal: bool) -> String {
let mut header = vec!["year".to_owned()];
header.extend(compared.iter().map(ComparedPlan::label));
let first = compared
.iter()
.filter_map(|plan| plan.projection.years.first().map(|row| row.year))
.min();
let last = compared
.iter()
.filter_map(|plan| plan.projection.years.last().map(|row| row.year))
.max();
let (Some(first), Some(last)) = (first, last) else {
return align(&header, &[]);
};
let rows: Vec<Vec<String>> = (first..=last)
.map(|year| {
let mut cells = vec![year.to_string()];
for plan in compared {
let cell = plan.projection.row(year).map_or_else(String::new, |row| {
display_dollars(metric.value(row), row.deflator, nominal)
});
cells.push(cell);
}
cells
})
.collect();
align(&header, &rows)
}
#[derive(Serialize)]
struct CompareReply<'a> {
plans: Vec<PlanReply<'a>>,
}
#[derive(Serialize)]
struct PlanReply<'a> {
path: String,
name: Option<&'a str>,
summary: Summary,
years: &'a [YearRow],
}
fn reply<'a>(compared: &'a [ComparedPlan], args: &CompareArgs) -> CompareReply<'a> {
CompareReply {
plans: compared
.iter()
.map(|plan| PlanReply {
path: plan.path.display().to_string(),
name: plan.name.as_deref(),
summary: plan.projection.summary(!args.nominal),
years: &plan.projection.years,
})
.collect(),
}
}