use super::report::{Kind, Line, Report};
const HEADER: [&str; 6] = ["PICK", "QTY", "PART", "REFS", "UNIT", "TOTAL"];
fn price(value: Option<f64>, estimated: bool) -> String {
let mark = if estimated { "~" } else { "" };
value.map_or_else(|| "-".into(), |v| format!("{mark}{v:.2}"))
}
fn label(line: &Line) -> String {
match line.kind {
Kind::Print => line.description.clone(),
_ => line.mpn.as_ref().map_or_else(
|| line.description.clone(),
|mpn| format!("{} ({mpn})", line.description),
),
}
}
fn row(line: &Line) -> [String; 6] {
[
if line.picked { "x" } else { "" }.into(),
line.quantity.to_string(),
label(line),
line.designators.join(" "),
price(line.unit_price, line.estimated),
price(line.total, line.estimated),
]
}
fn widths(rows: &[[String; 6]]) -> [usize; 6] {
let mut widths = HEADER.map(str::len);
for row in rows {
for (width, cell) in widths.iter_mut().zip(row) {
*width = (*width).max(cell.chars().count());
}
}
widths
}
fn pad(row: &[String; 6], widths: &[usize; 6]) -> String {
let cells = row.iter().zip(widths).map(|(c, w)| format!("{c:<w$}"));
cells.collect::<Vec<_>>().join(" ").trim_end().to_string()
}
pub fn table(report: &Report) -> String {
let mut rows = vec![HEADER.map(String::from)];
rows.extend(report.lines.iter().map(row));
let widths = widths(&rows);
let mut out: Vec<String> = rows.iter().map(|r| pad(r, &widths)).collect();
let t = &report.totals;
let unit = &report.currency;
out.push(String::new());
out.push(format!("electronics {:>10.2} {unit}", t.electronics));
out.push(format!("printed {:>10.2} {unit}", t.printed));
out.push(format!("other {:>10.2} {unit}", t.manual));
out.push(format!("TOTAL {:>10.2} {unit}", t.total));
if t.unpriced > 0 {
out.push(format!("{} picked line(s) have no price yet", t.unpriced));
}
if t.estimated > 0 {
out.push(format!(
"{} picked line(s) use an estimated price (~)",
t.estimated
));
}
out.join("\n") + "\n"
}
fn quote(cell: &str) -> String {
let guarded = if cell.starts_with(['=', '+', '-', '@']) {
format!("'{cell}")
} else {
cell.to_string()
};
if guarded.contains([',', '"', '\n']) {
format!("\"{}\"", guarded.replace('"', "\"\""))
} else {
guarded
}
}
pub fn csv(report: &Report) -> String {
let mut out = String::from(
"id,description,mpn,refs,quantity,unit_price,total,estimated,picked,supplier,url\n",
);
for line in &report.lines {
let cells = [
line.id.clone(),
line.description.clone(),
line.mpn.clone().unwrap_or_default(),
line.designators.join(" "),
line.quantity.to_string(),
line.unit_price
.map(|p| format!("{p:.4}"))
.unwrap_or_default(),
line.total.map(|t| format!("{t:.2}")).unwrap_or_default(),
line.estimated.to_string(),
line.picked.to_string(),
line.supplier.clone().unwrap_or_default(),
line.url.clone().unwrap_or_default(),
];
out.push_str(&cells.map(|c| quote(&c)).join(","));
out.push('\n');
}
out
}