use std::hint::black_box;
use std::time::{Duration, Instant};
use truecalc_core::{Engine, Ref, Resolver, Value as CoreValue};
use truecalc_workbook::{
Address, CellInput, EngineFlavor, RecalcContext, Value, Workbook, Worksheet,
};
struct FixedGrid;
impl Resolver for FixedGrid {
fn resolve(&mut self, r: &Ref) -> CoreValue {
match r {
Ref::Cell { addr, .. } => cell_value(addr.row, addr.col),
Ref::Range { start, end, .. } => {
let rows = (start.row..=end.row)
.map(|row| {
CoreValue::Array(
(start.col..=end.col)
.map(|col| cell_value(row, col))
.collect(),
)
})
.collect();
CoreValue::Array(rows)
}
_ => CoreValue::Empty,
}
}
}
fn cell_value(row: u32, col: u32) -> CoreValue {
match (row, col) {
(1..=5, 1) => CoreValue::Number(row as f64),
(1..=5, 2) => CoreValue::Number(row as f64 * 10.0),
_ => CoreValue::Empty,
}
}
const FORMULAS: &[&str] = &[
"=A1+B1",
"=SUM(A1:A5)",
"=SUMPRODUCT(A1:A5, B1:B5)",
"=IF(A1>0, \"pos\", \"neg\")",
"=AND(A1=1, B1=10)",
"=MAP(A1:A5, LAMBDA(x, x*2))",
"=REDUCE(0, A1:A5, LAMBDA(acc, x, acc+x))",
"=SCAN(0, A1:A5, LAMBDA(acc, x, acc+x))",
"=BYROW(A1:B5, LAMBDA(r, SUM(r)))",
"=MAKEARRAY(2, 3, LAMBDA(r, c, r*10+c))",
"=LAMBDA(x, x+1)(41)",
"=TEXTJOIN(\"-\", TRUE, A1:A5)",
"=VLOOKUP(3, A1:B5, 2, FALSE)",
"=SORT(B1:B5, 1, FALSE)",
"=FILTER(A1:A5, A1:A5>2)",
"=TODAY()",
"=NOW()",
"=RAND()",
"=RANDBETWEEN(1, 1000000)",
"=CONCATENATE(\"a\", TEXT(A1, \"0.00\"))",
"=SEQUENCE(3, 2)",
"=XLOOKUP(2, A1:A5, B1:B5)",
"=IFERROR(1/0, \"boom\")",
"=SUMIF(A1:A5, \">2\", B1:B5)",
];
const NOW_SERIAL: f64 = 46_000.25;
const NOW_UTC_NANOS: i64 = 1_780_878_600_000_000_000;
fn eval_one(engine: &Engine, formula: &str, index: usize) -> CoreValue {
let mut resolver = FixedGrid;
let rng_cell = Some((7_u64, 0_u32, index as u32 + 1, 1_u32));
engine.evaluate_with_resolver_at_keyed(
formula,
&mut resolver,
Some(NOW_SERIAL),
Some(NOW_UTC_NANOS),
rng_cell,
)
}
#[test]
fn reused_engine_matches_a_fresh_engine_per_formula() {
let shared = Engine::sheets();
for (i, formula) in FORMULAS.iter().enumerate() {
let fresh = Engine::sheets();
let expected = eval_one(&fresh, formula, i);
let actual = eval_one(&shared, formula, i);
assert_eq!(
actual, expected,
"reused engine diverged from a fresh engine on {formula}"
);
}
}
#[test]
fn shared_engine_is_order_and_repetition_independent() {
let shared = Engine::sheets();
let forward: Vec<CoreValue> = FORMULAS
.iter()
.enumerate()
.map(|(i, f)| eval_one(&shared, f, i))
.collect();
let mut backward: Vec<(usize, CoreValue)> = FORMULAS
.iter()
.enumerate()
.rev()
.map(|(i, f)| (i, eval_one(&shared, f, i)))
.collect();
backward.sort_by_key(|(i, _)| *i);
for (i, value) in &backward {
assert_eq!(
value, &forward[*i],
"reverse-order evaluation on the shared engine changed {}",
FORMULAS[*i]
);
}
for (i, formula) in FORMULAS.iter().enumerate() {
assert_eq!(
eval_one(&shared, formula, i),
forward[i],
"third pass on the shared engine changed {formula}"
);
}
}
#[test]
fn shared_engine_recalc_matches_one_engine_per_formula() {
let cases = [
"=A1+B1",
"=SUM(A1:A5)",
"=SUMPRODUCT(A1:A5, B1:B5)",
"=IF(A1>0, \"pos\", \"neg\")",
"=MAP(A1:A5, LAMBDA(x, x*2))",
"=REDUCE(0, A1:A5, LAMBDA(acc, x, acc+x))",
"=BYROW(A1:B5, LAMBDA(r, SUM(r)))",
"=TEXTJOIN(\"-\", TRUE, A1:A5)",
"=VLOOKUP(3, A1:B5, 2, FALSE)",
"=SORT(B1:B5, 1, FALSE)",
"=SUMIF(A1:A5, \">2\", B1:B5)",
"=IFERROR(1/0, \"boom\")",
"=TODAY()",
"=NOW()",
"=RAND()",
"=RANDBETWEEN(1, 1000000)",
];
let addr = |i: usize| Address::new(i as u32 * 8 + 1, 4).unwrap();
let ctx = RecalcContext::new(1_780_878_600_000, "UTC", 7).unwrap();
let mut shared = literal_grid();
for (i, formula) in cases.iter().enumerate() {
shared
.set(
"Sheet1",
addr(i),
CellInput::Formula((*formula).to_string()),
)
.unwrap();
}
shared.recalc(&ctx);
for (i, formula) in cases.iter().enumerate() {
let mut solo = literal_grid();
solo.set(
"Sheet1",
addr(i),
CellInput::Formula((*formula).to_string()),
)
.unwrap();
solo.recalc(&ctx);
assert_eq!(
shared.get("Sheet1", addr(i)).unwrap().value(),
solo.get("Sheet1", addr(i)).unwrap().value(),
"sharing the engine across cells changed the value of {formula}"
);
}
}
fn literal_grid() -> Workbook {
let mut wb = Workbook::new(EngineFlavor::Sheets);
wb.add_sheet(Worksheet::new("Sheet1")).unwrap();
for row in 1..=5u32 {
wb.set(
"Sheet1",
Address::new(row, 1).unwrap(),
CellInput::Literal(Value::Number(f64::from(row))),
)
.unwrap();
wb.set(
"Sheet1",
Address::new(row, 2).unwrap(),
CellInput::Literal(Value::Number(f64::from(row) * 10.0)),
)
.unwrap();
}
wb
}
fn registry_build_cost() -> Duration {
const N: u32 = 40;
black_box(Engine::sheets());
let start = Instant::now();
for _ in 0..N {
black_box(Engine::sheets());
}
start.elapsed() / N
}
fn build_chain(n: u32) -> Workbook {
let mut wb = Workbook::new(EngineFlavor::Sheets);
wb.add_sheet(Worksheet::new("Sheet1")).unwrap();
for row in 1..=n {
wb.set(
"Sheet1",
Address::new(row, 1).unwrap(),
CellInput::Literal(Value::Number(row as f64)),
)
.unwrap();
wb.set(
"Sheet1",
Address::new(row, 2).unwrap(),
CellInput::Formula(format!("=A{row}+1")),
)
.unwrap();
}
wb
}
#[test]
fn full_recalc_does_not_build_a_registry_per_cell() {
const CELLS: u32 = 200;
let unit = registry_build_cost();
let ctx = RecalcContext::new(0, "UTC", 0).unwrap();
let template = build_chain(CELLS);
let mut warm = template.clone();
warm.recalc(&ctx);
let mut wb = template.clone();
let start = Instant::now();
black_box(wb.recalc(&ctx));
let elapsed = start.elapsed();
let units = elapsed.as_secs_f64() / unit.as_secs_f64();
let budget = f64::from(CELLS) / 3.0;
assert!(
units <= budget,
"recalc of {CELLS} formula cells cost {units:.1} registry builds \
({elapsed:?} at {unit:?} per build); budget is {budget:.1}. \
An engine is being constructed per cell (issue #886)."
);
}