use formualizer_common::{ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::parse;
use crate::engine::{Engine, EvalConfig, FormulaPlaneMode};
use crate::test_workbook::TestWorkbook;
fn cases() -> Vec<(&'static str, LiteralValue)> {
vec![
("=COUNT(SORT(A1:A3))", LiteralValue::Number(3.0)),
("=COUNT(FILTER(A1:A3,B1:B3))", LiteralValue::Number(3.0)),
(
"=COUNT(FILTER(A1:A3,B1:B3=\"x\"))",
LiteralValue::Number(2.0),
),
(
"=SUM(FILTER(A1:A3,B1:B3=\"x\"))",
LiteralValue::Number(40.0),
),
(
"=SUM(FILTER(SEQUENCE(3),{TRUE;FALSE;TRUE}))",
LiteralValue::Number(4.0),
),
("=SUM(SORT(SEQUENCE(3)))", LiteralValue::Number(6.0)),
("=COUNT(TRANSPOSE({1,2}))", LiteralValue::Number(2.0)),
("=SUM(SORT({3;1;2}))", LiteralValue::Number(6.0)),
("=COUNT(UNIQUE({1;1;2}))", LiteralValue::Number(2.0)),
("=SUM(TAKE({1;2;3},2))", LiteralValue::Number(3.0)),
("=SUM(DROP({1;2;3},1))", LiteralValue::Number(5.0)),
("=SUM(SORTBY({3;1;2},{3;1;2}))", LiteralValue::Number(6.0)),
("=XLOOKUP(20,A1:A3*1,A1:A3)", LiteralValue::Number(20.0)),
("=XMATCH(20,A1:A3*1)", LiteralValue::Number(2.0)),
(
"=SUM(SORT(FILTER(A1:A3,B1:B3=\"x\")))",
LiteralValue::Number(40.0),
),
]
}
fn build(mode: FormulaPlaneMode) -> Engine<TestWorkbook> {
let mut engine = Engine::new(
TestWorkbook::default(),
EvalConfig::default().with_formula_plane_mode(mode),
);
for (row, value) in [(1, 10.0), (2, 20.0), (3, 30.0)] {
engine
.set_cell_value("Sheet1", row, 1, LiteralValue::Number(value))
.unwrap();
}
for (row, text) in [(1, "x"), (2, "y"), (3, "x")] {
engine
.set_cell_value("Sheet1", row, 2, LiteralValue::Text(text.into()))
.unwrap();
}
for (idx, (formula, _)) in cases().into_iter().enumerate() {
engine
.set_cell_formula("Sheet1", idx as u32 + 1, 5, parse(formula).unwrap())
.unwrap();
}
engine.evaluate_all().unwrap();
engine
}
#[test]
fn dynamic_array_builtins_accept_computed_array_arguments() {
for mode in [
FormulaPlaneMode::Off,
FormulaPlaneMode::AuthoritativeExperimental,
] {
let engine = build(mode);
for (idx, (formula, expected)) in cases().into_iter().enumerate() {
let got = engine.get_cell_value("Sheet1", idx as u32 + 1, 5);
assert_eq!(
got,
Some(expected),
"{formula} evaluated unexpectedly in {mode:?} mode"
);
}
}
}
#[test]
fn genuine_reference_errors_are_still_preserved() {
let mut engine = Engine::new(TestWorkbook::default(), EvalConfig::default());
engine
.set_cell_value("Sheet1", 1, 1, LiteralValue::Number(10.0))
.unwrap();
let cases = [
(1, "=SUM(SORT(OFFSET(A1,-1,0)))", ExcelErrorKind::Ref),
(
2,
"=SUM(FILTER(OFFSET(A1,-1,0),{TRUE}))",
ExcelErrorKind::Ref,
),
(
3,
"=COUNT(TRANSPOSE(INDIRECT(\"not a reference\")))",
ExcelErrorKind::Name,
),
];
for (row, formula, _) in cases {
engine
.set_cell_formula("Sheet1", row, 5, parse(formula).unwrap())
.unwrap();
}
engine.evaluate_all().unwrap();
for (row, formula, expected_kind) in cases {
match engine.get_cell_value("Sheet1", row, 5) {
Some(LiteralValue::Error(error)) => {
assert_eq!(error.kind, expected_kind, "{formula}")
}
other => panic!("{formula}: expected an error value, got {other:?}"),
}
}
}
#[test]
fn computed_array_arguments_are_evaluated_once() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug)]
struct CountingArrayFn(Arc<AtomicUsize>);
impl crate::function::Function for CountingArrayFn {
fn caps(&self) -> crate::function::FnCaps {
crate::function::FnCaps::PURE
}
fn name(&self) -> &'static str {
"COUNTINGARRAY"
}
fn min_args(&self) -> usize {
0
}
fn eval<'a, 'b, 'c>(
&self,
_args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
_ctx: &dyn crate::traits::FunctionContext<'b>,
) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
self.0.fetch_add(1, Ordering::SeqCst);
Ok(crate::traits::CalcValue::Scalar(LiteralValue::Array(vec![
vec![LiteralValue::Number(3.0)],
vec![LiteralValue::Number(1.0)],
vec![LiteralValue::Number(2.0)],
])))
}
}
for formula in [
"=SUM(SORT(COUNTINGARRAY()))",
"=SUM(UNIQUE(COUNTINGARRAY()))",
"=SUM(FILTER(COUNTINGARRAY(),{TRUE;TRUE;TRUE}))",
] {
let counter = Arc::new(AtomicUsize::new(0));
let workbook =
TestWorkbook::default().with_function(Arc::new(CountingArrayFn(counter.clone())));
let mut engine = Engine::new(workbook, EvalConfig::default());
engine
.set_cell_formula("Sheet1", 1, 1, parse(formula).unwrap())
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(6.0)),
"{formula}"
);
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"{formula} must evaluate its computed argument exactly once"
);
}
}
#[test]
fn whole_column_references_still_resolve() {
let mut engine = Engine::new(TestWorkbook::default(), EvalConfig::default());
for (row, value) in [(1, 10.0), (2, 20.0), (3, 30.0)] {
engine
.set_cell_value("Sheet1", row, 1, LiteralValue::Number(value))
.unwrap();
}
for (row, text) in [(1, "x"), (2, "y"), (3, "x")] {
engine
.set_cell_value("Sheet1", row, 2, LiteralValue::Text(text.into()))
.unwrap();
}
engine
.set_cell_formula(
"Sheet1",
1,
5,
parse("=SUM(FILTER(A:A,B:B=\"x\"))").unwrap(),
)
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 5),
Some(LiteralValue::Number(40.0))
);
}
#[test]
fn groupby_and_pivotby_accept_computed_arguments() {
let mut engine = Engine::new(TestWorkbook::default(), EvalConfig::default());
for (row, group, value) in [(1, "a", 1.0), (2, "b", 2.0), (3, "a", 3.0)] {
engine
.set_cell_value("Sheet1", row, 1, LiteralValue::Text(group.into()))
.unwrap();
engine
.set_cell_value("Sheet1", row, 2, LiteralValue::Number(value))
.unwrap();
}
for (row, formula) in [
(1, "=COUNT(GROUPBY(A1:A3,B1:B3*1,\"SUM\"))"),
(2, "=COUNT(PIVOTBY(A1:A3,A1:A3,B1:B3*1,\"SUM\"))"),
] {
engine
.set_cell_formula("Sheet1", row, 5, parse(formula).unwrap())
.unwrap();
}
engine.evaluate_all().unwrap();
for (row, formula) in [
(1, "=COUNT(GROUPBY(A1:A3,B1:B3*1,\"SUM\"))"),
(2, "=COUNT(PIVOTBY(A1:A3,A1:A3,B1:B3*1,\"SUM\"))"),
] {
match engine.get_cell_value("Sheet1", row, 5) {
Some(LiteralValue::Error(error)) => {
panic!("{formula} rejected its computed argument with {error:?}")
}
None => panic!("{formula} produced no value"),
Some(_) => {}
}
}
}
#[test]
fn computed_scalars_are_accepted_as_1x1_ranges() {
let mut engine = Engine::new(TestWorkbook::default(), EvalConfig::default());
engine
.set_cell_formula("Sheet1", 1, 5, parse("=COUNT(TRANSPOSE(1+1))").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 5),
Some(LiteralValue::Number(1.0))
);
}