use retiretui_engine::plan::{Dollars, Plan, TreatmentClass};
use retiretui_engine::project::{Summary, YearRow, deflate};
pub const SUMMARY_LABELS: [&str; 9] = [
"final net",
"peak",
"peak yr",
"taxes",
"medicare",
"conversions",
"unfunded",
"first unf",
"deferred",
];
pub fn summary_table(leading: &[&str], rows: Vec<(Vec<String>, Summary)>) -> String {
let header: Vec<String> = leading
.iter()
.chain(SUMMARY_LABELS.iter())
.map(|&label| label.to_owned())
.collect();
let rows: Vec<Vec<String>> = rows
.into_iter()
.map(|(mut cells, summary)| {
cells.extend(summary_cells(&summary));
cells
})
.collect();
align(&header, &rows)
}
pub fn summary_cells(summary: &Summary) -> Vec<String> {
vec![
plain_dollars(summary.final_net_worth),
plain_dollars(summary.peak_net_worth),
summary.peak_year.to_string(),
plain_dollars(summary.lifetime_taxes),
plain_dollars(summary.lifetime_medicare),
plain_dollars(summary.lifetime_conversions),
plain_dollars(summary.lifetime_unfunded),
summary
.first_unfunded_year
.map_or_else(|| "-".to_owned(), |year| year.to_string()),
plain_dollars(summary.final_deferred),
]
}
const THOUSANDS: usize = 3;
pub fn money(amount: Dollars) -> String {
let sign = if amount < 0 { "-" } else { "" };
format!("{sign}${}", grouped(amount.unsigned_abs()))
}
pub fn grouped(count: u64) -> String {
let digits = count.to_string();
let mut grouped = String::with_capacity(digits.len() + digits.len() / THOUSANDS);
for (at, digit) in digits.chars().enumerate() {
if at > 0 && (digits.len() - at).is_multiple_of(THOUSANDS) {
grouped.push(',');
}
grouped.push(digit);
}
grouped
}
pub fn account_name<'a>(plan: &'a Plan, id: &'a str) -> &'a str {
let named = plan.accounts.iter().find(|account| account.id == id);
named
.and_then(|account| account.name.as_deref())
.unwrap_or(id)
}
pub fn event_name<'a>(plan: &'a Plan, id: &'a str) -> &'a str {
let named = plan.events.iter().find(|event| event.id == id);
named.and_then(|event| event.name.as_deref()).unwrap_or(id)
}
pub fn income_name<'a>(plan: &'a Plan, id: &'a str) -> &'a str {
plan.income_source(id)
.and_then(|income| income.name.as_deref())
.unwrap_or(id)
}
const PERCENT: f64 = 100.0;
pub fn rate(rate: f64) -> String {
let percent = format!("{:.4}", rate * PERCENT);
let trimmed = percent.trim_end_matches('0').trim_end_matches('.');
format!("{trimmed}%")
}
pub fn plain_dollars(amount: Dollars) -> String {
amount.to_string()
}
pub fn ages_text(plan: &Plan, row: &YearRow) -> String {
let ages: Vec<String> = plan
.household
.people
.iter()
.map(|person| row.ages.get(&person.id).copied().unwrap_or(0).to_string())
.collect();
ages.join("/")
}
pub fn present_classes(plan: &Plan) -> Vec<TreatmentClass> {
TreatmentClass::ALL
.iter()
.copied()
.filter(|class| {
plan.accounts
.iter()
.any(|account| account.treatment() == *class)
})
.collect()
}
#[derive(Debug)]
pub enum Column {
Account(String),
Class(TreatmentClass),
}
pub fn year_figures(row: &YearRow, columns: &[Column]) -> Vec<Dollars> {
let mut figures = vec![
row.total_income,
row.expenses,
row.taxes.total,
row.total_withdrawals(),
];
figures.extend(columns.iter().map(|column| match column {
Column::Account(id) => row.balances.get(id).copied().unwrap_or(0),
Column::Class(class) => row.class_totals.get(*class),
}));
figures.push(row.net_worth);
figures
}
pub fn basis_amount(amount: Dollars, deflator: f64, nominal: bool) -> Dollars {
if nominal {
amount
} else {
deflate(amount, deflator)
}
}
pub fn display_dollars(amount: Dollars, deflator: f64, nominal: bool) -> String {
basis_amount(amount, deflator, nominal).to_string()
}
pub fn align(header: &[String], rows: &[Vec<String>]) -> String {
let mut widths: Vec<usize> = header.iter().map(String::len).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
widths[i] = widths[i].max(cell.len());
}
}
let mut lines = Vec::with_capacity(rows.len() + 1);
for cells in std::iter::once(header).chain(rows.iter().map(Vec::as_slice)) {
let line: Vec<String> = cells
.iter()
.enumerate()
.map(|(i, cell)| format!("{cell:>width$}", width = widths[i]))
.collect();
lines.push(line.join(" ").trim_end().to_owned());
}
lines.join("\n") + "\n"
}