use crate::engine::{Engine, EvalConfig};
use crate::test_workbook::TestWorkbook;
use chrono::NaiveDate;
use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::parse;
fn eval(engine: &mut Engine<TestWorkbook>, formula: &str) -> Option<LiteralValue> {
engine
.set_cell_formula("Sheet1", 1, 20, parse(formula).unwrap())
.unwrap();
engine.evaluate_all().unwrap();
engine.get_cell_value("Sheet1", 1, 20)
}
fn assert_number(value: Option<LiteralValue>, expected: f64, formula: &str) {
match value {
Some(LiteralValue::Int(i)) => assert_eq!(i as f64, expected, "{formula}"),
Some(LiteralValue::Number(n)) => assert!((n - expected).abs() < 1e-9, "{formula} => {n}"),
other => panic!("{formula}: expected {expected}, got {other:?}"),
}
}
fn assert_error(value: Option<LiteralValue>, expected: ExcelErrorKind, formula: &str) {
match value {
Some(LiteralValue::Error(e)) => assert_eq!(e.kind, expected, "{formula}"),
other => panic!("{formula}: expected {expected:?}, got {other:?}"),
}
}
fn assert_na(value: Option<LiteralValue>, formula: &str) {
assert_error(value, ExcelErrorKind::Na, formula);
}
fn build_engine() -> Engine<TestWorkbook> {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
fn num(engine: &mut Engine<TestWorkbook>, row: u32, col: u32, v: i64) {
engine
.set_cell_value("Sheet1", row, col, LiteralValue::Int(v))
.unwrap();
}
for i in 1..=5i64 {
num(&mut engine, i as u32, 1, i);
num(&mut engine, i as u32, 5, 6 - i);
}
engine
.set_cell_value("Sheet1", 1, 3, LiteralValue::Text("Header".into()))
.unwrap();
for i in 1..=5i64 {
num(&mut engine, (i as u32) + 1, 3, i);
}
num(&mut engine, 1, 7, 1);
num(&mut engine, 2, 7, 2);
num(&mut engine, 4, 7, 4);
num(&mut engine, 5, 7, 5);
engine
.set_cell_value("Sheet1", 1, 9, LiteralValue::Text("apple".into()))
.unwrap();
num(&mut engine, 2, 9, 1);
num(&mut engine, 3, 9, 2);
engine
.set_cell_value("Sheet1", 4, 9, LiteralValue::Text("zebra".into()))
.unwrap();
for (row, word) in [(1u32, "alpha"), (2, "beta"), (3, "gamma")] {
engine
.set_cell_value("Sheet1", row, 11, LiteralValue::Text(word.into()))
.unwrap();
}
for (row, v) in [(1u32, 3i64), (2, 1), (3, 5), (4, 2), (5, 4)] {
num(&mut engine, row, 13, v);
}
for i in 1..=5i64 {
num(&mut engine, i as u32, 2, i * 10);
}
engine
}
#[test]
fn blank_tail_does_not_make_an_ascending_range_unsorted() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(3,A1:A10,1)"),
3.0,
"MATCH(3,A1:A10,1)",
);
}
#[test]
fn blank_tail_is_never_the_selected_approximate_position() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(3.5,A1:A10,1)"),
3.0,
"MATCH(3.5,A1:A10,1)",
);
}
#[test]
fn needle_past_the_last_key_selects_the_last_populated_row() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(6,A1:A10,1)"),
5.0,
"MATCH(6,A1:A10,1)",
);
}
#[test]
fn vlookup_approximate_tolerates_a_blank_tail() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=VLOOKUP(3,A1:B10,2,TRUE)"),
30.0,
"VLOOKUP(3,A1:B10,2,TRUE)",
);
}
#[test]
fn interior_blank_is_skipped_and_positions_stay_original() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(4,G1:G5,1)"),
4.0,
"MATCH(4,G1:G5,1)",
);
assert_number(
eval(&mut engine, "=MATCH(3,G1:G5,1)"),
2.0,
"MATCH(3,G1:G5,1)",
);
}
#[test]
fn text_header_above_a_numeric_column_is_skipped() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(3,C1:C6,1)"),
4.0,
"MATCH(3,C1:C6,1)",
);
}
#[test]
fn text_header_is_skipped_for_a_between_keys_needle() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(3.5,C1:C6,1)"),
4.0,
"MATCH(3.5,C1:C6,1)",
);
}
#[test]
fn numeric_needle_skips_text_entries_on_both_sides() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(2,I1:I4,1)"),
3.0,
"MATCH(2,I1:I4,1)",
);
}
#[test]
fn text_needle_skips_numeric_entries() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(\"m\",I1:I4,1)"),
1.0,
"MATCH(\"m\",I1:I4,1)",
);
}
#[test]
fn text_vector_with_a_blank_tail_matches() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(\"beta\",K1:K10,1)"),
2.0,
"MATCH(\"beta\",K1:K10,1)",
);
}
#[test]
fn blank_tail_does_not_make_a_descending_range_unsorted() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(3,E1:E10,-1)"),
3.0,
"MATCH(3,E1:E10,-1)",
);
}
#[test]
fn genuinely_unsorted_data_is_still_na() {
let mut engine = build_engine();
assert_na(eval(&mut engine, "=MATCH(2,M1:M5,1)"), "MATCH(2,M1:M5,1)");
}
#[test]
fn needle_below_every_key_is_still_na() {
let mut engine = build_engine();
assert_na(
eval(&mut engine, "=MATCH(0.5,A1:A10,1)"),
"MATCH(0.5,A1:A10,1)",
);
}
#[test]
fn exact_match_over_a_blank_tail_is_unaffected() {
let mut engine = build_engine();
assert_number(
eval(&mut engine, "=MATCH(3,A1:A10,0)"),
3.0,
"MATCH(3,A1:A10,0)",
);
}
#[test]
fn approximate_lookups_propagate_errors_in_any_lookup_position() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
let error = LiteralValue::Error(ExcelError::new(ExcelErrorKind::Div));
for (row, value) in [
LiteralValue::Int(1),
LiteralValue::Int(2),
error.clone(),
LiteralValue::Int(9),
]
.into_iter()
.enumerate()
{
engine
.set_cell_value("Sheet1", row as u32 + 1, 1, value)
.unwrap();
engine
.set_cell_value(
"Sheet1",
row as u32 + 1,
2,
LiteralValue::Int((row as i64 + 1) * 10),
)
.unwrap();
}
assert_error(
eval(&mut engine, "=MATCH(5,A1:A4,1)"),
ExcelErrorKind::Div,
"MATCH ascending deciding error",
);
assert_error(
eval(&mut engine, "=VLOOKUP(5,A1:B4,2,TRUE)"),
ExcelErrorKind::Div,
"VLOOKUP deciding error",
);
engine
.set_cell_value("Sheet1", 3, 1, LiteralValue::Int(9))
.unwrap();
engine
.set_cell_value("Sheet1", 4, 1, error.clone())
.unwrap();
assert_error(
eval(&mut engine, "=MATCH(5,A1:A4,1)"),
ExcelErrorKind::Div,
"MATCH ascending non-deciding error",
);
assert_error(
eval(&mut engine, "=VLOOKUP(5,A1:B4,2,TRUE)"),
ExcelErrorKind::Div,
"VLOOKUP non-deciding error",
);
for (col, value) in [
LiteralValue::Int(1),
LiteralValue::Int(2),
LiteralValue::Int(9),
error.clone(),
]
.into_iter()
.enumerate()
{
engine
.set_cell_value("Sheet1", 10, col as u32 + 1, value)
.unwrap();
engine
.set_cell_value(
"Sheet1",
11,
col as u32 + 1,
LiteralValue::Int((col as i64 + 1) * 10),
)
.unwrap();
}
assert_error(
eval(&mut engine, "=HLOOKUP(5,A10:D11,2,TRUE)"),
ExcelErrorKind::Div,
"HLOOKUP non-deciding error",
);
for (row, value) in [
LiteralValue::Int(9),
error,
LiteralValue::Int(2),
LiteralValue::Int(1),
]
.into_iter()
.enumerate()
{
engine
.set_cell_value("Sheet1", row as u32 + 1, 5, value)
.unwrap();
}
assert_error(
eval(&mut engine, "=MATCH(5,E1:E4,-1)"),
ExcelErrorKind::Div,
"MATCH descending deciding error",
);
}
#[test]
fn approximate_match_propagates_materialized_pre_1900_date_error() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
for (row, date) in [(1899, 1, 1), (1899, 6, 1), (1900, 3, 1), (2024, 1, 1)]
.into_iter()
.enumerate()
{
engine
.set_cell_value(
"Sheet1",
row as u32 + 1,
1,
LiteralValue::Date(NaiveDate::from_ymd_opt(date.0, date.1, date.2).unwrap()),
)
.unwrap();
}
assert_error(
eval(&mut engine, "=MATCH(50000,A1:A4,1)"),
ExcelErrorKind::Num,
"MATCH over pre-1900 date errors",
);
}
#[test]
fn ascending_duplicates_are_sorted_and_return_the_last_original_position() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
for (row, value) in [(1, 1), (2, 2), (4, 2), (6, 2), (8, 5)] {
engine
.set_cell_value("Sheet1", row, 8, LiteralValue::Int(value))
.unwrap();
}
assert_number(
eval(&mut engine, "=MATCH(3,H1:H8,1)"),
6.0,
"MATCH duplicate ascending keys",
);
}
#[test]
fn vlookup_maps_a_projected_match_back_to_the_original_row() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
engine
.set_cell_value("Sheet1", 1, 1, LiteralValue::Text("Key".into()))
.unwrap();
for value in 1..=5i64 {
engine
.set_cell_value("Sheet1", value as u32 + 1, 1, LiteralValue::Int(value))
.unwrap();
engine
.set_cell_value("Sheet1", value as u32 + 1, 2, LiteralValue::Int(value * 10))
.unwrap();
}
assert_number(
eval(&mut engine, "=VLOOKUP(3.5,A1:B6,2,TRUE)"),
30.0,
"VLOOKUP original-position remap",
);
}
#[test]
fn searchable_count_selects_the_small_descending_linear_path() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
for (row, value) in (1..=5i64).rev().enumerate() {
engine
.set_cell_value("Sheet1", row as u32 + 1, 5, LiteralValue::Int(value))
.unwrap();
}
assert_number(
eval(&mut engine, "=MATCH(3.5,E1:E10,-1)"),
2.0,
"MATCH threshold uses searchable count",
);
}
#[test]
fn descending_binary_search_returns_the_last_qualifying_position() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
for (row, value) in (1..=10i64).rev().enumerate() {
engine
.set_cell_value("Sheet1", row as u32 + 1, 1, LiteralValue::Int(value))
.unwrap();
}
assert_number(
eval(&mut engine, "=MATCH(3.5,A1:A10,-1)"),
7.0,
"MATCH descending binary boundary",
);
assert_number(
eval(&mut engine, "=MATCH(3,A1:A10,-1)"),
8.0,
"MATCH descending exact-hit masking control",
);
}
#[test]
fn descending_binary_search_skips_interior_blanks_without_shifting_the_answer() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
for (row, value) in [
(1, 9),
(2, 8),
(3, 7),
(5, 6),
(6, 5),
(7, 4),
(9, 3),
(10, 2),
(11, 1),
] {
engine
.set_cell_value("Sheet1", row, 7, LiteralValue::Int(value))
.unwrap();
}
assert_number(
eval(&mut engine, "=MATCH(5.5,G1:G11,-1)"),
5.0,
"MATCH descending with interior blanks",
);
}
#[test]
fn searchable_count_keeps_duplicate_tie_break_on_small_descending_data() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
for (row, value) in [(1, 5), (2, 4), (3, 4), (4, 3), (5, 2)] {
engine
.set_cell_value("Sheet1", row, 5, LiteralValue::Int(value))
.unwrap();
}
assert_number(
eval(&mut engine, "=MATCH(4,E1:E10,-1)"),
3.0,
"MATCH threshold preserves descending duplicate tie-break",
);
}
#[test]
fn searchable_count_not_range_extent_controls_descending_tie_break() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
for (row, value) in [(1, 5), (3, 4), (4, 4), (6, 3), (8, 2)] {
engine
.set_cell_value("Sheet1", row, 5, LiteralValue::Int(value))
.unwrap();
}
for row in [2, 5, 7, 9, 10] {
engine
.set_cell_value("Sheet1", row, 5, LiteralValue::Text("skip".into()))
.unwrap();
}
assert_number(
eval(&mut engine, "=MATCH(4,E1:E10,-1)"),
4.0,
"MATCH threshold uses projected length, not range extent",
);
}