use super::common::arrow_eval_config;
use crate::engine::named_range::{NameScope, NamedDefinition};
use crate::engine::{Engine, EvalConfig};
use crate::reference::{CellRef, Coord};
use crate::test_workbook::TestWorkbook;
use formualizer_common::LiteralValue;
use formualizer_parse::parser::parse;
const SHEET: &str = "S";
const FIRST: u32 = 9;
const SEED: f64 = 18000.0;
#[derive(Clone, Copy, Debug)]
enum Mode {
Candidate { parallel: bool },
NoFamily { parallel: bool },
Reference,
}
fn config(mode: Mode) -> EvalConfig {
match mode {
Mode::Candidate { parallel } => EvalConfig {
enable_parallel: parallel,
..arrow_eval_config()
},
Mode::NoFamily { parallel } => EvalConfig {
enable_parallel: parallel,
family_execution: false,
..arrow_eval_config()
},
Mode::Reference => EvalConfig {
enable_parallel: false,
family_execution: false,
family_lift: false,
formula_compression: false,
..arrow_eval_config()
},
}
}
const MODES: [Mode; 4] = [
Mode::Candidate { parallel: false },
Mode::Candidate { parallel: true },
Mode::NoFamily { parallel: false },
Mode::NoFamily { parallel: true },
];
#[derive(Clone, Copy, Debug)]
enum AnchorRow {
Above,
Below,
}
#[derive(Clone, Copy, Debug)]
enum AnchorKind {
Constant,
Formula,
FormulaOtherSheet,
Empty,
}
#[derive(Clone, Debug)]
struct Layout {
n: u32,
cols: Vec<u32>,
anchor_col: u32,
anchor_row: AnchorRow,
anchor_kind: AnchorKind,
template: &'static str,
}
fn letter(col: u32) -> String {
assert!((1..=26).contains(&col));
char::from(b'A' + (col - 1) as u8).to_string()
}
impl Layout {
fn anchor_row(&self) -> u32 {
match self.anchor_row {
AnchorRow::Above => 4,
AnchorRow::Below => FIRST + self.n + 5,
}
}
fn last_row(&self) -> u32 {
(FIRST + self.n + 6).max(self.anchor_row() + 1)
}
fn formula(&self, col: u32, row: u32) -> String {
self.template
.replace("{c}", &letter(col))
.replace("{a}", &letter(self.anchor_col))
.replace("{ar1}", &(self.anchor_row() - 1).to_string())
.replace("{ar}", &self.anchor_row().to_string())
.replace("{p}", &(row - 1).to_string())
.replace("{l}", &(row - 5).to_string())
.replace("{r}", &row.to_string())
}
fn build(&self, mode: Mode) -> Engine<TestWorkbook> {
let mut e = Engine::new(TestWorkbook::new(), config(mode));
let ar = self.anchor_row();
let a = self.anchor_col;
for r in 1..=self.last_row() {
e.set_cell_value(SHEET, r, 1, LiteralValue::Number(f64::from(r % 7) + 0.5))
.unwrap();
e.set_cell_value("Other", r, a, LiteralValue::Number(0.01 * f64::from(r)))
.unwrap();
}
e.set_cell_value(SHEET, ar - 1, a, LiteralValue::Number(0.2))
.unwrap();
match self.anchor_kind {
AnchorKind::Constant => e
.set_cell_value(SHEET, ar, a, LiteralValue::Number(0.2 / 12.0))
.unwrap(),
AnchorKind::Formula => e
.set_cell_formula(
SHEET,
ar,
a,
parse(format!("=+{}{}/12", letter(a), ar - 1)).unwrap(),
)
.unwrap(),
AnchorKind::FormulaOtherSheet => e
.set_cell_formula(
SHEET,
ar,
a,
parse(format!("=Other!{}{}*2", letter(a), ar)).unwrap(),
)
.unwrap(),
AnchorKind::Empty => {}
}
if self.template.contains("rate") {
let sheet_id = e.sheet_id(SHEET).unwrap();
e.define_name(
"rate",
NamedDefinition::Cell(CellRef::new(sheet_id, Coord::from_excel(ar, a, true, true))),
NameScope::Workbook,
)
.unwrap();
}
for &c in &self.cols {
e.set_cell_value(SHEET, FIRST - 1, c, LiteralValue::Number(SEED))
.unwrap();
for r in FIRST..FIRST + self.n {
e.set_cell_formula(SHEET, r, c, parse(self.formula(c, r)).unwrap())
.unwrap();
}
}
e
}
}
fn key(v: Option<LiteralValue>) -> String {
match v {
Some(LiteralValue::Number(x)) => format!("N{:016x}", x.to_bits()),
Some(LiteralValue::Error(e)) => format!("E{:?}", e.kind),
other => format!("{other:?}"),
}
}
fn assert_same(layout: &Layout, a: &Engine<TestWorkbook>, b: &Engine<TestWorkbook>, ctx: &str) {
let max_col = layout
.cols
.iter()
.copied()
.max()
.unwrap()
.max(layout.anchor_col);
for r in 1..=layout.last_row() {
for c in 1..=max_col {
let (x, y) = (a.get_cell_value(SHEET, r, c), b.get_cell_value(SHEET, r, c));
assert_eq!(
key(x.clone()),
key(y.clone()),
"{ctx}: {SHEET}!{}{r}: {x:?} vs per-cell {y:?}\n{layout:?}",
letter(c)
);
}
}
}
fn edit(layout: &Layout, e: &mut Engine<TestWorkbook>, step: usize) {
let (ar, a) = (layout.anchor_row(), layout.anchor_col);
match step {
0 => e
.set_cell_value(SHEET, ar - 1, a, LiteralValue::Number(0.6))
.unwrap(),
1 => {
for &c in &layout.cols {
e.set_cell_value(SHEET, FIRST - 1, c, LiteralValue::Number(500.0))
.unwrap();
}
}
2 => e
.set_cell_value("Other", ar, a, LiteralValue::Number(0.75))
.unwrap(),
_ => unreachable!(),
}
}
fn check(layout: &Layout, modes: &[Mode], edits: usize) -> u64 {
let mut reference = layout.build(Mode::Reference);
reference.evaluate_all().unwrap();
let mut engines: Vec<(Mode, Engine<TestWorkbook>)> =
modes.iter().map(|&m| (m, layout.build(m))).collect();
for (mode, e) in &mut engines {
e.evaluate_all().unwrap();
assert_same(layout, e, &reference, &format!("{mode:?} first eval"));
}
for step in 0..edits {
edit(layout, &mut reference, step);
reference.evaluate_all().unwrap();
for (mode, e) in &mut engines {
edit(layout, e, step);
e.evaluate_all().unwrap();
assert_same(layout, e, &reference, &format!("{mode:?} edit {step}"));
}
}
engines
.iter()
.map(|(_, e)| e.chained_members_for_test())
.sum()
}
const TEMPLATES: &[&str] = &[
"=+{c}{p}-({c}{p}*${a}${ar})",
"={c}{p}*(1-{a}${ar})",
"={c}{p}+${a}{p}",
"={c}{p}-{c}{l}/100",
"={c}{p}-{c}$8*{a}${ar}",
"={c}{p}*(1-S!${a}${ar})",
"={c}{p}*(1-Other!${a}${ar})",
"={c}{p}*(1-SUM(${a}${ar1}:${a}${ar})/10)",
"={c}{p}*(1-rate)",
"={c}{p}*(1-INDEX(${a}${ar1}:${a}${ar},2))",
"={c}{p}+$A{r}*${a}${ar}",
"=-(-{c}{p})*(1+${a}${ar})^1",
];
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn pick<T: Copy>(&mut self, xs: &[T]) -> T {
xs[(self.next() % xs.len() as u64) as usize]
}
}
#[test]
fn decay_schedule_reads_same_column_anchor() {
for n in [31, 32, 33, 64, 1000] {
for (cols, anchor_kind) in [
(vec![2], AnchorKind::Formula),
(vec![2], AnchorKind::Constant),
(vec![5], AnchorKind::Formula),
] {
let layout = Layout {
n,
cols,
anchor_col: 2,
anchor_row: AnchorRow::Above,
anchor_kind,
template: TEMPLATES[0],
};
for mode in MODES {
let mut e = layout.build(mode);
e.evaluate_all().unwrap();
let c = layout.cols[0];
let mut expect = SEED;
for r in FIRST..FIRST + n {
expect -= expect * (0.2 / 12.0);
match e.get_cell_value(SHEET, r, c) {
Some(LiteralValue::Number(x)) => assert!(
(x - expect).abs() <= 1e-9 * expect.abs(),
"{mode:?} n={n} {layout:?} R{r}: {x} vs {expect}"
),
other => panic!("{mode:?} n={n} R{r}: {other:?}"),
}
}
assert_eq!(
e.get_cell_value(SHEET, FIRST, c),
Some(LiteralValue::Number(17700.0))
);
}
}
}
}
#[test]
fn decay_schedule_keeps_chain_lift() {
let layout = Layout {
n: 64,
cols: vec![2],
anchor_col: 2,
anchor_row: AnchorRow::Above,
anchor_kind: AnchorKind::Formula,
template: TEMPLATES[0],
};
for n in [33u32, 64, 1000] {
let layout = Layout {
n,
..layout.clone()
};
let mut e = layout.build(Mode::Candidate { parallel: false });
e.evaluate_all().unwrap();
let chained = e.chained_members_for_test();
assert!(chained + 3 >= u64::from(n), "n={n}: {chained} chained");
}
}
#[test]
fn chain_reads_match_per_cell_over_generated_layouts() {
let mut chained = 0u64;
for &template in TEMPLATES {
for anchor_row in [AnchorRow::Above, AnchorRow::Below] {
for anchor_kind in [
AnchorKind::Constant,
AnchorKind::Formula,
AnchorKind::FormulaOtherSheet,
] {
for cols in [vec![2], vec![5], vec![2, 3]] {
let layout = Layout {
n: 40,
cols,
anchor_col: 2,
anchor_row,
anchor_kind,
template,
};
chained += check(&layout, &MODES[..2], 1);
}
}
}
}
assert!(chained > 0, "the generated layouts exercise the chain lift");
}
#[test]
fn chain_reads_match_per_cell_over_random_layouts() {
let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
for _ in 0..48 {
let anchor_col = rng.pick(&[2u32, 3, 4]);
let layout = Layout {
n: rng.pick(&[2u32, 3, 31, 32, 33, 47, 64, 65]),
cols: rng
.pick(&[&[2u32][..], &[3], &[4], &[2, 3], &[3, 4], &[2, 4]])
.to_vec(),
anchor_col,
anchor_row: rng.pick(&[AnchorRow::Above, AnchorRow::Below]),
anchor_kind: rng.pick(&[
AnchorKind::Constant,
AnchorKind::Formula,
AnchorKind::FormulaOtherSheet,
AnchorKind::Empty,
]),
template: rng.pick(TEMPLATES),
};
check(&layout, &MODES, 3);
}
}