use formualizer_common::{ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::parse;
use crate::engine::{Engine, EvalConfig, FormulaPlaneMode};
use crate::test_workbook::TestWorkbook;
fn n(v: f64) -> LiteralValue {
LiteralValue::Number(v)
}
fn t(s: &str) -> LiteralValue {
LiteralValue::Text(s.into())
}
fn na() -> LiteralValue {
LiteralValue::Error(ExcelErrorKind::Na.into())
}
fn build(mode: FormulaPlaneMode) -> Engine<TestWorkbook> {
let mut e = Engine::new(
TestWorkbook::default(),
EvalConfig::default().with_formula_plane_mode(mode),
);
for (i, d) in ["x", "y", "z", "x", "y", "q"].iter().enumerate() {
e.set_cell_value("Sheet1", i as u32 + 1, 4, t(d)).unwrap();
}
for (i, h) in ["x", "y", "z"].iter().enumerate() {
let k = i as u32 + 1;
e.set_cell_value("Sheet1", k, 8, t(h)).unwrap();
e.set_cell_value("Sheet1", k, 9, n(10.0 * k as f64))
.unwrap();
e.set_cell_value("Sheet1", 5, 7 + k, t(h)).unwrap();
e.set_cell_value("Sheet1", 6, 7 + k, n(10.0 * k as f64))
.unwrap();
}
e
}
fn column(values: &[LiteralValue]) -> Vec<Vec<LiteralValue>> {
values.iter().map(|v| vec![v.clone()]).collect()
}
fn cases() -> Vec<(&'static str, Vec<Vec<LiteralValue>>)> {
vec![
(
"=MATCH(D1:D6,H1:H3,0)",
column(&[n(1.0), n(2.0), n(3.0), n(1.0), n(2.0), na()]),
),
("=COUNT(MATCH(D1:D6,H1:H3,0))", vec![vec![n(5.0)]]),
("=MATCH({15,25},I1:I3,1)", vec![vec![n(1.0), n(2.0)]]),
(
"=VLOOKUP(D1:D3,H1:I3,2,FALSE)",
column(&[n(10.0), n(20.0), n(30.0)]),
),
(
"=VLOOKUP(D1:D6,H1:I3,2,FALSE)",
column(&[n(10.0), n(20.0), n(30.0), n(10.0), n(20.0), na()]),
),
(
"=SUM(IFERROR(VLOOKUP(D1:D6,H1:I3,2,FALSE),0))",
vec![vec![n(90.0)]],
),
(
"=HLOOKUP({\"z\",\"x\"},H5:J6,2,FALSE)",
vec![vec![n(30.0), n(10.0)]],
),
(
"=LOOKUP(D1:D3,H1:H3,I1:I3)",
column(&[n(10.0), n(20.0), n(30.0)]),
),
("=VLOOKUP(\"y\",H1:I3,2,FALSE)", vec![vec![n(20.0)]]),
]
}
fn same(got: &LiteralValue, want: &LiteralValue) -> bool {
match (got, want) {
(LiteralValue::Number(a), LiteralValue::Number(b)) => (a - b).abs() < 1e-9,
(LiteralValue::Int(a), LiteralValue::Number(b)) => (*a as f64 - b).abs() < 1e-9,
(LiteralValue::Error(a), LiteralValue::Error(b)) => a.kind == b.kind,
_ => got == want,
}
}
#[test]
fn array_lookup_values_lift_match_vlookup_hlookup_lookup() {
for mode in [
FormulaPlaneMode::Off,
FormulaPlaneMode::AuthoritativeExperimental,
] {
let mut e = build(mode);
let cases = cases();
for (i, (formula, _)) in cases.iter().enumerate() {
e.set_cell_formula("Sheet1", 20 + 10 * i as u32, 12, parse(formula).unwrap())
.unwrap();
}
e.evaluate_all().unwrap();
for (i, (formula, want)) in cases.iter().enumerate() {
let base = 20 + 10 * i as u32;
for (r, row) in want.iter().enumerate() {
for (c, w) in row.iter().enumerate() {
let got = e
.get_cell_value("Sheet1", base + r as u32, 12 + c as u32)
.unwrap_or(LiteralValue::Empty);
assert!(
same(&got, w),
"{formula} [{r},{c}] = {got:?}, Excel gives {w:?} ({mode:?})"
);
}
}
let below = e.get_cell_value("Sheet1", base + want.len() as u32, 12);
let right = e.get_cell_value("Sheet1", base, 12 + want[0].len() as u32);
assert!(
matches!(below, None | Some(LiteralValue::Empty))
&& matches!(right, None | Some(LiteralValue::Empty)),
"{formula} spilled past its shape: below {below:?}, right {right:?} ({mode:?})"
);
}
}
}
#[test]
fn memoized_single_value_lookups_are_unchanged() {
let mut e = build(FormulaPlaneMode::Off);
for row in 1..=6u32 {
e.set_cell_formula(
"Sheet1",
row,
14,
parse(format!("=IFERROR(VLOOKUP(D{row},$H$1:$I$3,2,FALSE),0)")).unwrap(),
)
.unwrap();
}
e.set_cell_formula(
"Sheet1",
1,
16,
parse("=SUM(IFERROR(VLOOKUP(D1:D6,$H$1:$I$3,2,FALSE),0))").unwrap(),
)
.unwrap();
e.evaluate_all().unwrap();
let want = [10.0, 20.0, 30.0, 10.0, 20.0, 0.0];
for (i, w) in want.iter().enumerate() {
let got = e.get_cell_value("Sheet1", i as u32 + 1, 14);
assert!(
same(&got.clone().unwrap_or(LiteralValue::Empty), &n(*w)),
"N{} = {got:?}",
i + 1
);
}
let total = e
.get_cell_value("Sheet1", 1, 16)
.unwrap_or(LiteralValue::Empty);
assert!(same(&total, &n(90.0)), "array total = {total:?}");
}
#[test]
fn array_lookup_reads_long_vectors_once_for_all_lookup_values() {
const ROWS: u32 = 10_000;
const NEEDLES: u32 = 1_000;
let mut e = Engine::new(TestWorkbook::default(), EvalConfig::default());
for row in 1..=ROWS {
e.set_cell_value("Sheet1", row, 1, n(f64::from(row)))
.unwrap();
e.set_cell_value("Sheet1", row, 2, n(f64::from(row) * 2.0))
.unwrap();
}
let formula = format!("=SUM(LOOKUP(A1:A{NEEDLES},A1:A{ROWS},B1:B{ROWS}))");
e.set_cell_formula("Sheet1", 1, 4, parse(&formula).unwrap())
.unwrap();
e.evaluate_all().unwrap();
let want = f64::from(NEEDLES * (NEEDLES + 1));
let got = e.get_cell_value("Sheet1", 1, 4).unwrap();
assert!(same(&got, &n(want)), "{got:?}");
}
#[test]
fn array_lookup_keeps_each_form_and_a_miss_per_element() {
let mut e = build(FormulaPlaneMode::Off);
let cases = [
("=SUM(LOOKUP({\"y\",\"z\"},H1:I3))", 50.0),
("=SUM(LOOKUP({\"y\",\"z\"},H5:J6))", 50.0),
("=SUM(IFERROR(LOOKUP({5,15},I1:I3,H6:J6),100))", 110.0),
];
for (i, (formula, _)) in cases.iter().enumerate() {
e.set_cell_formula("Sheet1", 40 + i as u32, 12, parse(formula).unwrap())
.unwrap();
}
e.evaluate_all().unwrap();
for (i, (formula, want)) in cases.iter().enumerate() {
let got = e.get_cell_value("Sheet1", 40 + i as u32, 12).unwrap();
assert!(same(&got, &n(*want)), "{formula} = {got:?}");
}
}