use calamine::{open_workbook, CellErrorType, Data, Reader, Xlsx};
use ganit_core::{evaluate, ErrorKind, Value};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
fn fixture(milestone: &str, name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(milestone)
.join(name)
}
fn parse_error_string(s: &str) -> Option<ErrorKind> {
match s {
"#DIV/0!" => Some(ErrorKind::DivByZero),
"#VALUE!" => Some(ErrorKind::Value),
"#REF!" => Some(ErrorKind::Ref),
"#NAME?" => Some(ErrorKind::Name),
"#NUM!" => Some(ErrorKind::Num),
"#N/A" => Some(ErrorKind::NA),
"#NULL!" => Some(ErrorKind::Null),
_ => None,
}
}
fn oracle_to_value(cell: &Data) -> Option<Value> {
match cell {
Data::Float(f) => Some(Value::Number(*f)),
Data::Int(i) => Some(Value::Number(*i as f64)),
Data::String(s) => {
if let Some(kind) = parse_error_string(s.trim()) {
Some(Value::Error(kind))
} else {
Some(Value::Text(s.clone()))
}
}
Data::Bool(b) => Some(Value::Bool(*b)),
Data::Error(e) => Some(Value::Error(match e {
CellErrorType::Div0 => ErrorKind::DivByZero,
CellErrorType::Value => ErrorKind::Value,
CellErrorType::Ref => ErrorKind::Ref,
CellErrorType::Name => ErrorKind::Name,
CellErrorType::Num => ErrorKind::Num,
CellErrorType::NA => ErrorKind::NA,
CellErrorType::Null => ErrorKind::Null,
_ => return None,
})),
Data::Empty | Data::DateTimeIso(_) | Data::DurationIso(_) | Data::DateTime(_) => None,
}
}
fn decode_xlsx_escapes(s: &str) -> String {
let mut result = String::new();
let mut rest = s;
while let Some(start) = rest.find("_x") {
result.push_str(&rest[..start]);
let after = &rest[start + 2..];
if let Some(end) = after.find('_') {
let hex = &after[..end];
if hex.len() == 4 && hex.chars().all(|c| c.is_ascii_hexdigit()) {
if let Ok(n) = u32::from_str_radix(hex, 16) {
if let Some(c) = char::from_u32(n) {
result.push(c);
rest = &after[end + 1..];
continue;
}
}
}
}
result.push_str("_x");
rest = after;
}
result.push_str(rest);
result
}
fn values_match(actual: &Value, expected: &Value) -> bool {
match (actual, expected) {
(Value::Number(a), Value::Number(b)) => {
(a - b).abs() <= b.abs() * 1e-4 + 1e-10
}
(Value::Date(a), Value::Number(b)) => {
(a - b).abs() <= b.abs() * 1e-4 + 1e-10
}
(Value::Text(s), Value::Number(b)) => {
if let Ok(v) = s.trim().parse::<f64>() {
(v - b).abs() <= b.abs() * 1e-9 + 1e-10
} else {
false
}
}
(Value::Text(s), Value::Text(e)) if e.is_empty() => {
s.chars().all(|c| (c as u32) < 32)
}
(Value::Text(s), Value::Text(e)) => {
decode_xlsx_escapes(e) == *s
}
_ => actual == expected,
}
}
fn run_fixture(path: &Path) {
assert!(path.exists(), "fixture not found: {:?}", path);
let mut workbook: Xlsx<_> = open_workbook(path)
.unwrap_or_else(|e| panic!("failed to open {:?}: {}", path, e));
let sheet_names: Vec<String> = workbook.sheet_names().to_vec();
let vars: HashMap<String, Value> = HashMap::new();
let mut failures: Vec<String> = Vec::new();
let mut total = 0usize;
for sheet_name in &sheet_names {
let range = workbook
.worksheet_range(sheet_name)
.unwrap_or_else(|e| panic!("failed to read sheet {sheet_name}: {e}"));
for (row_idx, row) in range.rows().enumerate().skip(1) {
if row.len() < 3 {
continue;
}
let desc = match &row[0] {
Data::String(s) => s.as_str(),
_ => continue,
};
let formula = match &row[1] {
Data::String(s) => s.as_str(),
_ => continue,
};
let expected = match oracle_to_value(&row[2]) {
Some(v) => v,
None => continue,
};
total += 1;
let actual = evaluate(formula, &vars);
if !values_match(&actual, &expected) {
failures.push(format!(
" FAIL [{sheet_name}] row {} {desc}\n formula: {formula}\n expected: {expected:?}\n actual: {actual:?}",
row_idx + 2,
));
}
}
}
if !failures.is_empty() {
panic!(
"\n{}/{} conformance failures in {}:\n\n{}\n",
failures.len(),
total,
path.file_name().unwrap().to_string_lossy(),
failures.join("\n\n"),
);
}
}
macro_rules! conformance_test {
($fn_name:ident, $milestone:literal, $file:literal) => {
#[test]
fn $fn_name() {
run_fixture(&fixture($milestone, $file));
}
};
(pending, $fn_name:ident, $milestone:literal, $file:literal) => {
#[test]
#[ignore = "pending implementation — run with --include-ignored to see failures"]
fn $fn_name() {
run_fixture(&fixture($milestone, $file));
}
};
}
conformance_test!(m1_math_conformance, "m1", "Math.xlsx");
conformance_test!(m1_logical_conformance, "m1", "Logical.xlsx");
conformance_test!(m1_info_conformance, "m1", "Info.xlsx");
conformance_test!(m1_statistical_conformance, "m1", "Statistical.xlsx");
conformance_test!(m1_operator_conformance, "m1", "Operator.xlsx");
conformance_test!(m1_text_conformance, "m1", "Text.xlsx");
conformance_test!(m2_date_conformance, "m2", "Date.xlsx");
conformance_test!(pending, m2_engineering_conformance, "m2", "Engineering.xlsx");
conformance_test!(pending, m2_info_conformance, "m2", "Info.xlsx");
conformance_test!(pending, m2_logical_conformance, "m2", "Logical.xlsx");
conformance_test!(pending, m2_lookup_conformance, "m2", "Lookup.xlsx");
conformance_test!(pending, m2_math_conformance, "m2", "Math.xlsx");
conformance_test!(m2_parser_conformance, "m2", "Parser.xlsx");
conformance_test!(m2_statistical_conformance, "m2", "Statistical.xlsx");
conformance_test!(pending, m2_text_conformance, "m2", "Text.xlsx");
conformance_test!(pending, m3_database_conformance, "m3", "Database.xlsx");
conformance_test!(pending, m3_engineering_conformance, "m3", "Engineering.xlsx");
conformance_test!(pending, m3_financial_conformance, "m3", "Financial.xlsx");
conformance_test!(pending, m3_info_conformance, "m3", "Info.xlsx");
conformance_test!(pending, m3_lookup_conformance, "m3", "Lookup.xlsx");
conformance_test!(pending, m3_math_conformance, "m3", "Math.xlsx");
conformance_test!(pending, m3_statistical_conformance, "m3", "Statistical.xlsx");
conformance_test!(pending, m4_array_conformance, "m4", "Array.xlsx");
conformance_test!(pending, m4_filter_conformance, "m4", "Filter.xlsx");
conformance_test!(pending, m4_info_conformance, "m4", "Info.xlsx");
conformance_test!(pending, m4_logical_conformance, "m4", "Logical.xlsx");
conformance_test!(pending, m4_lookup_conformance, "m4", "Lookup.xlsx");
conformance_test!(pending, m4_math_conformance, "m4", "Math.xlsx");
conformance_test!(pending, m4_operator_conformance, "m4", "Operator.xlsx");
conformance_test!(pending, m4_web_conformance, "m4", "Web.xlsx");