use formualizer_common::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 build(mode: FormulaPlaneMode) -> Engine<TestWorkbook> {
let mut e = Engine::new(
TestWorkbook::default(),
EvalConfig::default().with_formula_plane_mode(mode),
);
for (i, a) in ["a", "b", "a", "c", "b", "a"].iter().enumerate() {
let row = i as u32 + 1;
e.set_cell_value("Sheet1", row, 1, t(a)).unwrap();
e.set_cell_value("Sheet1", row, 2, n(row as f64)).unwrap();
}
e.set_cell_value("Sheet1", 1, 6, t("a")).unwrap();
e.set_cell_value("Sheet1", 2, 6, t("b")).unwrap();
e
}
fn cases() -> Vec<(&'static str, Vec<Vec<LiteralValue>>)> {
vec![
(
"=SUM(SUMIFS(B1:B6,A1:A6,{\"a\",\"b\"}))",
vec![vec![n(17.0)]],
),
(
"=SUMPRODUCT(COUNTIFS(A1:A6,{\"a\",\"b\"}))",
vec![vec![n(5.0)]],
),
(
"=SUMIF(A1:A6,F1:F2,B1:B6)",
vec![vec![n(10.0)], vec![n(7.0)]],
),
("=COUNTIF(A1:A6,F1:F2)", vec![vec![n(3.0)], vec![n(2.0)]]),
(
"=COUNTIFS(A1:A6,{\"a\",\"b\"},B1:B6,{\">1\";\">3\"})",
vec![vec![n(2.0), n(2.0)], vec![n(1.0), n(1.0)]],
),
(
"=COUNTIFS(A1:A6,{\"a\",\"b\",\"c\"},B1:B6,{\">0\",\">3\"})",
vec![vec![n(3.0), n(1.0), n(0.0)]],
),
(
"=AVERAGEIFS(B1:B6,A1:A6,{\"a\";\"b\"})",
vec![vec![n(10.0 / 3.0)], vec![n(3.5)]],
),
("=SUM(1/COUNTIF(A1:A6,A1:A6))", vec![vec![n(3.0)]]),
("=FILTER(F1:F2,COUNTIF(A1:A6,F1:F2)>2)", vec![vec![t("a")]]),
("=COUNTIFS(A1:A6,{\"a\",#N/A})", vec![vec![n(3.0), n(0.0)]]),
(
"=SUMIFS(B1:B6,A1:A6,{\"a\",#N/A})",
vec![vec![n(10.0), n(0.0)]],
),
("=COUNTIF(A1:A6,{\"a\"})", vec![vec![n(3.0)]]),
]
}
fn close(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,
_ => got == want,
}
}
#[test]
fn array_criteria_lift_criteria_aggregates() {
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() {
for (r, row) in want.iter().enumerate() {
for (c, w) in row.iter().enumerate() {
let got = e
.get_cell_value("Sheet1", 20 + 10 * i as u32 + r as u32, 12 + c as u32)
.unwrap_or(LiteralValue::Empty);
assert!(
close(&got, w),
"{formula} [{r},{c}] = {got:?}, Excel gives {w:?} ({mode:?})"
);
}
}
let below = e.get_cell_value("Sheet1", 20 + 10 * i as u32 + want.len() as u32, 12);
assert!(
matches!(below, None | Some(LiteralValue::Empty)),
"{formula} spilled too far down: {below:?} ({mode:?})"
);
}
}
}
#[test]
fn array_countif_agrees_with_single_criterion_countif() {
use chrono::NaiveDate;
let date = NaiveDate::from_ymd_opt(2002, 2, 1).unwrap();
let serial = LiteralValue::Date(date).as_serial_number().unwrap();
let cells = [
n(5.0),
LiteralValue::Int(5),
n(5.0 + 1e-13),
n(5.0 + 1e-11),
t("5"),
t("05"),
t("5.0"),
t("Ab"),
t("aB"),
t("ab "),
t(""),
LiteralValue::Empty,
LiteralValue::Boolean(true),
LiteralValue::Boolean(false),
t("TRUE"),
n(1.0),
n(0.0),
LiteralValue::Date(date),
n(serial),
t("2/1/2002"),
LiteralValue::Error(formualizer_common::ExcelErrorKind::Na.into()),
];
let cells = [cells.as_slice(), cells.as_slice()].concat();
let last = cells.len() as u32;
let literals = [
"5",
"\"5\"",
"\"5.0\"",
"\"05\"",
"\"=5\"",
"\"ab\"",
"\"AB\"",
"\"ab \"",
"\"\"",
"\"=\"",
"\"<>\"",
"TRUE",
"FALSE",
"\"true\"",
"1",
"0",
"\"2/1/2002\"",
"\">1\"",
"\"a*\"",
"\"<>ab\"",
];
let literals = [literals.as_slice(), literals.as_slice()].concat();
let array = format!("{{{}}}", literals.join(","));
for mode in [
FormulaPlaneMode::Off,
FormulaPlaneMode::AuthoritativeExperimental,
] {
let mut e = Engine::new(
TestWorkbook::default(),
EvalConfig::default().with_formula_plane_mode(mode),
);
for (i, cell) in cells.iter().enumerate() {
e.set_cell_value("Sheet1", i as u32 + 1, 1, cell.clone())
.unwrap();
}
let mut pairs = Vec::new();
let mut formula = |row: u32, col: u32, text: String| {
e.set_cell_formula("Sheet1", row, col, parse(&text).unwrap())
.unwrap();
};
formula(101, 3, format!("=COUNTIF(A1:A{last},A1:A{last})"));
for row in
(1..=last).filter(|row| !matches!(cells[*row as usize - 1], LiteralValue::Error(_)))
{
formula(row, 3, format!("=COUNTIF(A1:A{last},A{row})"));
pairs.push(((row, 3), (100 + row, 3)));
}
for (block, range) in [
(201, format!("A1:A{last}")),
(301, "A:A".into()),
(401, "AZ:AZ".into()),
] {
formula(block, 4, format!("=COUNTIF({range},{array})"));
for (i, literal) in literals.iter().enumerate() {
let col = 4 + i as u32;
formula(block + 1, col, format!("=COUNTIF({range},{literal})"));
pairs.push(((block, col), (block + 1, col)));
}
}
e.evaluate_all().unwrap();
for ((r1, c1), (r2, c2)) in pairs {
let (a, b) = (
e.get_cell_value("Sheet1", r1, c1),
e.get_cell_value("Sheet1", r2, c2),
);
assert_eq!(a, b, "R{r1}C{c1} and R{r2}C{c2} differ ({mode:?})");
}
}
}
#[test]
fn array_countif_folds_case_as_single_criterion_countif() {
let texts = [
"οσ", "ΟΣ", "ος", "Σ", "σ", "ς", "\u{212A}", "k", "K", "ſ", "s", "Straße", "STRASSE", "ẞ",
"ß", "İ", "i", "café", "CAFÉ", "£m", "£M", "DŽ", "Dž", "dž",
];
let texts = [texts.as_slice(), texts.as_slice()].concat();
let last = texts.len() as u32;
let literals: Vec<String> = ["ΟΣ", "k", "S", "ß", "ẞ", "i", "İ", "CAFÉ", "£m", "dž", "ss"]
.iter()
.map(|text| format!("\"{text}\""))
.collect();
let literals = [
literals.as_slice(),
literals.as_slice(),
literals.as_slice(),
]
.concat();
let array = format!("{{{}}}", literals.join(","));
for mode in [
FormulaPlaneMode::Off,
FormulaPlaneMode::AuthoritativeExperimental,
] {
let mut e = Engine::new(
TestWorkbook::default(),
EvalConfig::default().with_formula_plane_mode(mode),
);
for (i, text) in texts.iter().enumerate() {
e.set_cell_value("Sheet1", i as u32 + 1, 1, t(text))
.unwrap();
}
let mut pairs = Vec::new();
let mut formula = |row: u32, col: u32, text: String| {
e.set_cell_formula("Sheet1", row, col, parse(&text).unwrap())
.unwrap();
};
formula(101, 3, format!("=COUNTIF(A1:A{last},A1:A{last})"));
for row in 1..=last {
formula(row, 3, format!("=COUNTIF(A1:A{last},A{row})"));
pairs.push(((row, 3), (100 + row, 3)));
}
for (block, range) in [(201, format!("A1:A{last}")), (301, "A1:A1".into())] {
formula(block, 4, format!("=COUNTIF({range},{array})"));
for (i, literal) in literals.iter().enumerate() {
let col = 4 + i as u32;
formula(block + 1, col, format!("=COUNTIF({range},{literal})"));
pairs.push(((block, col), (block + 1, col)));
}
}
e.evaluate_all().unwrap();
for ((r1, c1), (r2, c2)) in pairs {
let (a, b) = (
e.get_cell_value("Sheet1", r1, c1),
e.get_cell_value("Sheet1", r2, c2),
);
assert_eq!(a, b, "R{r1}C{c1} and R{r2}C{c2} differ ({mode:?})");
}
assert_eq!(e.get_cell_value("Sheet1", 301, 4), Some(n(1.0)), "{mode:?}");
}
}
#[test]
fn distinct_count_idiom_over_a_long_column() {
const ROWS: u32 = 5_000;
let mut e = Engine::new(TestWorkbook::default(), EvalConfig::default());
for row in 1..=ROWS {
e.set_cell_value("Sheet1", row, 1, t(&format!("k{}", row % 1_000)))
.unwrap();
}
let formula = format!("=SUM(1/COUNTIF(A1:A{ROWS},A1:A{ROWS}))");
e.set_cell_formula("Sheet1", 1, 3, parse(&formula).unwrap())
.unwrap();
e.evaluate_all().unwrap();
let got = e
.get_cell_value("Sheet1", 1, 3)
.unwrap_or(LiteralValue::Empty);
assert!(close(&got, &n(1_000.0)), "{got:?}");
}