use std::collections::BTreeMap;
use vla_lang::calc::{agrees, Calc, CellId, Computed, Extent, Library, Names, Reason, Shapes};
use vla_lang::rows::{datum, quoted, Row, Sink, Value};
use vla_lang::sheet::{cell_ref, parse_a1_range, Cell, Content, Sheet, Workbook};
use super::{open, Discard};
use crate::messages::Refusal;
#[derive(Debug, Default)]
pub struct Collected {
pub model: Workbook,
pub cached: BTreeMap<CellId, Value>,
}
impl Collected {
fn place(&mut self, sheet: &str, addr: &str) -> Option<CellId> {
let i = self.model.ensure_sheet(sheet);
let r = parse_a1_range(addr)?;
Some((i, r.top, r.left))
}
}
impl Sink for Collected {
fn row(&mut self, row: &Row<'_>) {
match row {
Row::Sheet { name, .. } => {
self.model.ensure_sheet(name);
}
Row::Name { name, refers_to } => {
self.model
.defined_names
.push((name.to_string(), refers_to.to_string()));
}
Row::Table { .. } | Row::Refers { .. } => {}
Row::Cell { sheet, addr, value } => {
let Some(id) = self.place(sheet, addr) else {
return;
};
let content = match value {
Value::Number(text) => match text.trim().parse::<f64>() {
Ok(n) if n.is_finite() => Content::Number(n),
_ => Content::Text(text.clone()),
},
Value::Text(t) => Content::Text(t.clone()),
Value::Bool(b) => Content::Bool(*b),
Value::Error(e) => Content::Error(e.clone()),
Value::Date(d) => Content::Text(d.clone()),
};
self.model.sheets[id.0].set(id.1, id.2, Cell { content, style: 0 });
self.cached.insert(id, (*value).clone());
}
Row::Formula { sheet, addr, text } => {
let Some(id) = self.place(sheet, addr) else {
return;
};
let body = text.strip_prefix('=').unwrap_or(text);
self.model.sheets[id.0].set(
id.1,
id.2,
Cell {
content: Content::Formula(body.to_string()),
style: 0,
},
);
}
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Counts {
pub formulas: u64,
pub computed: u64,
pub not_computed: u64,
pub cycles: u64,
pub agree: u64,
pub differ: u64,
pub unchecked: u64,
}
impl Counts {
pub fn line(&self) -> String {
format!(
"formulas {} computed {} not-computed {} cycles {} agree {} differ {} unchecked {}",
self.formulas,
self.computed,
self.not_computed,
self.cycles,
self.agree,
self.differ,
self.unchecked
)
}
}
pub fn collect(bytes: &[u8], label: &str) -> Result<Collected, Refusal> {
let package = open(bytes, label)?;
let mut collected = Collected::default();
package.header_rows(&mut collected);
for i in 0..package.sheets().len() {
package.walk_sheet(i, &mut collected)?;
}
let _ = Discard;
Ok(collected)
}
pub fn census_of(collected: &Collected, library: &Library) -> String {
let model = &collected.model;
let extents: Vec<Extent> = model.sheets.iter().map(Sheet::extent).collect();
let plan = Shapes::of(
model,
model.sheets.len(),
library,
&Names::of(model),
&extents,
);
plan.census(model, library)
}
pub fn rows_of(collected: &Collected, library: &Library) -> (String, Counts) {
let calc = Calc::run(&collected.model, library);
let mut out = String::new();
let mut counts = Counts::default();
for cycle in calc.cycles() {
counts.cycles += 1;
let cells: Vec<String> = cycle
.iter()
.map(|id| quoted(&calc.spell_cell(*id)))
.collect();
out.push_str(&format!("(cycle {})\n", cells.join(" ")));
}
for id in calc.formula_cells() {
counts.formulas += 1;
let sheet = &collected.model.sheets[id.0].name;
let addr = cell_ref(id.1, id.2);
let formula = format!("={}", calc.formula(id).unwrap_or(""));
let computed = calc
.computed(id)
.cloned()
.unwrap_or(Computed::NotComputed(Reason::Cycle));
let cached = collected.cached.get(&id);
let verdict = match (&computed, cached) {
(Computed::Value(v), Some(c)) => match agrees(v, c) {
Some(true) => "agree",
Some(false) => "differ",
None => "unchecked",
},
_ => "unchecked",
};
match &computed {
Computed::Value(_) => counts.computed += 1,
Computed::NotComputed(_) => counts.not_computed += 1,
}
match verdict {
"agree" => counts.agree += 1,
"differ" => counts.differ += 1,
_ => counts.unchecked += 1,
}
out.push_str(&format!(
"(calc {} {} {} {} {} {})\n",
quoted(sheet),
quoted(&addr),
quoted(&formula),
computed.spell(),
cached.map(datum).unwrap_or_else(|| "none".to_string()),
verdict
));
}
(out, counts)
}
pub fn calc_text(bytes: &[u8], label: &str, library: &Library) -> Result<String, Refusal> {
let collected = collect(bytes, label)?;
Ok(rows_of(&collected, library).0)
}
pub fn calc_counts(bytes: &[u8], label: &str, library: &Library) -> Result<Counts, Refusal> {
let collected = collect(bytes, label)?;
Ok(rows_of(&collected, library).1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::excel;
const SAVED: &[u8] = include_bytes!("../../../scripts/reflect/saved.xlsx");
const FIXTURE: &[u8] = include_bytes!("../../../scripts/reflect/fixture.xlsx");
const BUILT: &[u8] = include_bytes!("../../../scripts/build/fixture_golden.xlsx");
#[test]
fn the_file_excel_saved_agrees_on_every_formula() {
let lib = excel::library();
let text = calc_text(SAVED, "saved.xlsx", &lib).unwrap();
let rows: Vec<&str> = text.lines().collect();
assert_eq!(rows.len(), 5, "{text}");
for row in &rows {
assert!(row.ends_with(" agree)"), "{row}");
}
assert!(
rows.contains(&"(calc \"Output\" \"C2\" \"=B2+B3\" 15 15 agree)"),
"{text}"
);
assert!(
rows.contains(&"(calc \"Output\" \"D2\" \"=IFS(B2>3,\\\"big\\\",TRUE,\\\"small\\\")\" \"big\" \"big\" agree)"),
"{text}"
);
let counts = calc_counts(SAVED, "saved.xlsx", &lib).unwrap();
assert_eq!(
counts,
Counts {
formulas: 5,
computed: 5,
not_computed: 0,
cycles: 0,
agree: 5,
differ: 0,
unchecked: 0
}
);
}
#[test]
fn the_core_s_own_build_holds_the_values_it_computes_and_agrees() {
let lib = excel::library();
let text = calc_text(BUILT, "fixture_golden.xlsx", &lib).unwrap();
for row in text.lines() {
assert!(row.ends_with(" agree)"), "{row}");
}
assert!(
text.contains("(calc \"Output\" \"B3\" \"=B2*2\" 10 10 agree)"),
"{text}"
);
}
#[test]
fn the_reflect_fixture_names_what_this_slice_does_not_compute() {
let lib = excel::library();
let text = calc_text(FIXTURE, "fixture.xlsx", &lib).unwrap();
let has = |s: &str| assert!(text.contains(s), "missing {s} in\n{text}");
has("(calc \"Model\" \"C1\" \"=B1*2\" 2400 2400 agree)");
has("(calc \"Model\" \"D1\" \"=SUM(B:B)\"");
has("(not-computed \"Sales[Amount]\")");
has("(not-computed \"SEQUENCE\")");
has("(not-computed \"INDIRECT\")");
has("(not-computed \"OFFSET\")");
has("(not-computed \"[Rates.xlsx]Sheet1!A1\")");
has("(not-computed \"B1:B2\")");
has("(calc \"Model\" \"E5\" \"=1/0\" (error \"#DIV/0!\") (error \"#DIV/0!\") agree)");
has("(calc \"Model\" \"B4\" \"=B3*Rate\" 80 80 agree)");
has("(calc \"Model\" \"G2\" \"=Local\" 800 800 agree)");
has("(calc \"Model\" \"D4\" \"=SUM(Model:Scratch!B1)\"");
let counts = calc_counts(FIXTURE, "fixture.xlsx", &lib).unwrap();
assert_eq!(counts.differ, 0, "{text}");
assert_eq!(counts.cycles, 0);
assert!(counts.agree >= 15, "{}", counts.line());
}
}