use datacraft_core::expr::{ExprError, Scope, parse};
use datacraft_core::schema::FieldValue;
fn eval(source: &str) -> Result<FieldValue, ExprError> {
parse(source)?.evaluate(&Scope::new())
}
fn eval_with(source: &str, scope: &Scope) -> Result<FieldValue, ExprError> {
parse(source)?.evaluate(scope)
}
fn ok(source: &str) -> FieldValue {
eval(source).unwrap_or_else(|e| panic!("{source:?} must evaluate: {e}"))
}
fn is_runtime_error(source: &str, needle: &str) {
let error = eval(source).expect_err("source must fail at runtime");
match error {
ExprError::Runtime { detail } => {
assert!(
detail.contains(needle),
"{source:?}: {detail:?} lacks {needle:?}"
)
}
other => panic!("{source:?}: expected runtime error, got {other:?}"),
}
}
fn is_syntax_error(source: &str) {
match eval(source) {
Err(ExprError::Lex { .. } | ExprError::Parse { .. }) => {}
other => panic!("{source:?}: expected a syntax error, got {other:?}"),
}
}
#[test]
fn literals_parse_and_evaluate() {
assert_eq!(ok("42"), FieldValue::Integer(42));
assert_eq!(ok("-7"), FieldValue::Integer(-7));
assert_eq!(ok("2.5"), FieldValue::Float(2.5));
assert_eq!(ok("\"Ada\""), FieldValue::Text("Ada".into()));
assert_eq!(ok("'Ada'"), FieldValue::Text("Ada".into()));
assert_eq!(
ok("\"say \"\"hi\"\"\""),
FieldValue::Text("say \"hi\"".into())
);
assert_eq!(ok("True"), FieldValue::Boolean(true));
assert_eq!(ok("False"), FieldValue::Boolean(false));
assert_eq!(ok("Null"), FieldValue::Null);
assert_eq!(ok("#2025-01-05#"), FieldValue::DateTime(45_662.0));
assert_eq!(
ok("#2025-01-05 13:45#"),
FieldValue::DateTime(45_662.0 + 825.0 / 1440.0)
);
}
#[test]
fn precedence_and_associativity_follow_the_documented_lattice() {
assert_eq!(ok("2+3*4"), FieldValue::Integer(14));
assert_eq!(ok("(2+3)*4"), FieldValue::Integer(20));
assert_eq!(ok("2^3^2"), FieldValue::Float(512.0)); assert_eq!(ok("-2^2"), FieldValue::Float(-4.0)); assert_eq!(ok("7\\2"), FieldValue::Integer(3));
assert_eq!(ok("7 Mod 2"), FieldValue::Integer(1));
assert_eq!(ok("\"a\" & 1+2"), FieldValue::Text("a3".into())); assert_eq!(ok("2 < 3 And 4 < 5"), FieldValue::Boolean(true));
assert_eq!(ok("Not 2 < 3"), FieldValue::Boolean(false));
}
#[test]
fn field_references_resolve_case_insensitively() {
let mut scope = Scope::new();
scope.set("Qty", FieldValue::Integer(3));
scope.set("UnitPrice", FieldValue::Currency(125_000));
scope.set_field("Order Details", "Qty", FieldValue::Integer(4));
assert_eq!(ok_with("[Qty]", &scope), FieldValue::Integer(3));
assert_eq!(ok_with("[qty]", &scope), FieldValue::Integer(3));
assert_eq!(
ok_with("[UnitPrice]*2", &scope),
FieldValue::Currency(250_000)
);
assert_eq!(
ok_with("[Order Details].[Qty] * 2", &scope),
FieldValue::Integer(8)
);
is_runtime_error("Ghost + 1", "Ghost");
is_runtime_error("[Nope].[Qty] * 2", "Nope");
}
fn ok_with(source: &str, scope: &Scope) -> FieldValue {
eval_with(source, scope).unwrap_or_else(|e| panic!("{source:?} must evaluate: {e}"))
}
#[test]
fn expressions_drive_a_calculated_row() {
let mut scope = Scope::new();
scope.set("Qty", FieldValue::Integer(3));
scope.set("UnitPrice", FieldValue::Currency(125_000));
assert_eq!(
ok_with("[Qty] * [UnitPrice]", &scope),
FieldValue::Currency(375_000)
);
assert_eq!(
ok_with("IIf([Qty] Is Null, 0, [Qty] * [UnitPrice])", &scope),
FieldValue::Currency(375_000)
);
}
#[test]
fn arithmetic_coerces_across_the_numeric_tower() {
assert_eq!(ok("1 + 2.5"), FieldValue::Float(3.5));
assert_eq!(ok("2 * 3"), FieldValue::Integer(6));
assert_eq!(ok("7 / 2"), FieldValue::Float(3.5)); let mut scope = Scope::new();
scope.set("c", FieldValue::Currency(10_000));
assert_eq!(ok_with("[c] + 1", &scope), FieldValue::Currency(20_000));
assert_eq!(ok("True + 1"), FieldValue::Integer(0)); assert_eq!(ok("\"5\" + 1"), FieldValue::Integer(6)); assert_eq!(ok("-7\\2"), FieldValue::Integer(-3)); assert_eq!(ok("7.4 Mod 2"), FieldValue::Integer(1)); is_runtime_error("\"a\" + 1", "a");
is_runtime_error("1/0", "zero");
is_runtime_error("7\\0", "zero");
is_runtime_error("1 Mod 0", "zero");
}
#[test]
fn concatenation_and_null_semantics() {
assert_eq!(ok("\"a\" & Null"), FieldValue::Text("a".into()));
assert_eq!(ok("Null & \"b\""), FieldValue::Text("b".into()));
assert_eq!(ok("Null & Null"), FieldValue::Text(String::new()));
assert_eq!(ok("\"a\" + Null"), FieldValue::Null);
assert_eq!(ok("Null + Null"), FieldValue::Null);
assert_eq!(ok("\"5\" + \"5\""), FieldValue::Text("55".into()));
assert_eq!(ok("1 + Null"), FieldValue::Null);
assert_eq!(ok("-Null"), FieldValue::Null);
let mut scope = Scope::new();
scope.set("n", FieldValue::Null);
assert_eq!(ok_with("[n] & \"!\"", &scope), FieldValue::Text("!".into()));
}
#[test]
fn comparisons_are_three_valued() {
assert_eq!(ok("1 = 1.0"), FieldValue::Boolean(true));
assert_eq!(ok("1 < 2.5"), FieldValue::Boolean(true));
assert_eq!(ok("\"a\" < \"b\""), FieldValue::Boolean(true));
assert_eq!(ok("\"A\" = \"a\""), FieldValue::Boolean(false)); assert_eq!(ok("Null = Null"), FieldValue::Null);
assert_eq!(ok("Null <> 1"), FieldValue::Null);
assert_eq!(ok("1 > Null"), FieldValue::Null);
}
#[test]
fn logic_propagates_null_three_valued() {
assert_eq!(ok("Not Null"), FieldValue::Null);
assert_eq!(ok("True And Null"), FieldValue::Null);
assert_eq!(ok("False And Null"), FieldValue::Boolean(false));
assert_eq!(ok("True Or Null"), FieldValue::Boolean(true));
assert_eq!(ok("False Or Null"), FieldValue::Null);
assert_eq!(ok("Not True"), FieldValue::Boolean(false));
assert_eq!(ok("1 And 2"), FieldValue::Boolean(true)); }
#[test]
fn like_understands_access_wildcards() {
assert_eq!(ok("\"K-42\" Like \"K-*\""), FieldValue::Boolean(true));
assert_eq!(ok("\"k-42\" Like \"K-*\""), FieldValue::Boolean(true)); assert_eq!(ok("\"abc\" Like \"a?c\""), FieldValue::Boolean(true));
assert_eq!(ok("\"a1\" Like \"a#\""), FieldValue::Boolean(true));
assert_eq!(ok("\"ab\" Like \"a#\""), FieldValue::Boolean(false));
assert_eq!(ok("\"m\" Like \"[m-p]\""), FieldValue::Boolean(true));
assert_eq!(ok("\"z\" Like \"[!m-p]\""), FieldValue::Boolean(true));
assert_eq!(ok("\"a*b\" Like \"a[*]b\""), FieldValue::Boolean(true));
assert_eq!(ok("Null Like \"a*\""), FieldValue::Null);
assert_eq!(ok("\"a\" Like Null"), FieldValue::Null);
}
#[test]
fn between_in_and_is_null_follow_jet_rules() {
assert_eq!(ok("5 Between 1 And 10"), FieldValue::Boolean(true));
assert_eq!(ok("1 Between 2 And 3"), FieldValue::Boolean(false));
assert_eq!(ok("Null Between 1 And 2"), FieldValue::Null);
assert_eq!(ok("1 Between Null And 3"), FieldValue::Null);
assert_eq!(ok("2 In (1, 2, 3)"), FieldValue::Boolean(true));
assert_eq!(ok("4 In (1, 2, 3)"), FieldValue::Boolean(false));
assert_eq!(ok("Null In (1, 2)"), FieldValue::Null);
let mut scope = Scope::new();
scope.set("x", FieldValue::Null);
assert_eq!(ok_with("[x] Is Null", &scope), FieldValue::Boolean(true));
assert_eq!(
ok_with("[x] Is Not Null", &scope),
FieldValue::Boolean(false)
);
}
#[test]
fn iif_and_nz_respect_null_conditions() {
assert_eq!(
ok("IIf(1 < 2, \"yes\", \"no\")"),
FieldValue::Text("yes".into())
);
assert_eq!(ok("IIf(Null, 1, 2)"), FieldValue::Integer(2)); assert_eq!(ok("IIf(0, 1, 2)"), FieldValue::Integer(2));
assert_eq!(ok("IIf(42, 1, 2)"), FieldValue::Integer(1));
assert_eq!(ok("iif(True, 1, 2)"), FieldValue::Integer(1)); assert_eq!(ok("Nz(Null)"), FieldValue::Text(String::new()));
assert_eq!(ok("Nz(Null, 5)"), FieldValue::Integer(5));
assert_eq!(ok("Nz(7, 9)"), FieldValue::Integer(7));
assert_eq!(ok("Nz(Null, \"x\")"), FieldValue::Text("x".into()));
}
#[test]
fn string_functions_cover_the_documented_family() {
assert_eq!(ok("Len(\"abc\")"), FieldValue::Integer(3));
assert_eq!(ok("Len(Null)"), FieldValue::Null);
assert_eq!(ok("Left(\"abcd\", 2)"), FieldValue::Text("ab".into()));
assert_eq!(ok("Right(\"abcd\", 2)"), FieldValue::Text("cd".into()));
assert_eq!(ok("Mid(\"abcdef\", 2, 3)"), FieldValue::Text("bcd".into()));
assert_eq!(ok("Mid(\"abcdef\", 3)"), FieldValue::Text("cdef".into()));
assert_eq!(ok("UCase(\"aB\")"), FieldValue::Text("AB".into()));
assert_eq!(ok("LCase(\"aB\")"), FieldValue::Text("ab".into()));
assert_eq!(ok("Trim(\" a \")"), FieldValue::Text("a".into()));
assert_eq!(ok("LTrim(\" a\")"), FieldValue::Text("a".into()));
assert_eq!(ok("RTrim(\"a \")"), FieldValue::Text("a".into()));
assert_eq!(ok("InStr(\"abcabc\", \"b\")"), FieldValue::Integer(2));
assert_eq!(ok("Asc(\"A\")"), FieldValue::Integer(65));
assert_eq!(ok("Chr(65)"), FieldValue::Text("A".into()));
assert_eq!(ok("IsNull(Null)"), FieldValue::Boolean(true));
assert_eq!(ok("IsNull(0)"), FieldValue::Boolean(false));
assert_eq!(ok("IsNumeric(\"5\")"), FieldValue::Boolean(true));
assert_eq!(ok("IsNumeric(\"a\")"), FieldValue::Boolean(false));
assert_eq!(ok("IsNumeric(Null)"), FieldValue::Null);
assert_eq!(ok("CStr(2.5)"), FieldValue::Text("2.5".into()));
assert_eq!(ok("CStr(True)"), FieldValue::Text("True".into()));
}
#[test]
fn date_functions_read_ole_components() {
assert_eq!(ok("Year(#2025-03-07 13:45#)"), FieldValue::Integer(2025));
assert_eq!(ok("Month(#2025-03-07 13:45#)"), FieldValue::Integer(3));
assert_eq!(ok("Day(#2025-03-07 13:45#)"), FieldValue::Integer(7));
assert_eq!(ok("Hour(#2025-03-07 13:45#)"), FieldValue::Integer(13));
assert_eq!(ok("Minute(#2025-03-07 13:45#)"), FieldValue::Integer(45));
assert_eq!(ok("Year(#2025-03-07#)"), FieldValue::Integer(2025));
assert_eq!(ok("DateSerial(2025, 1, 5)"), FieldValue::DateTime(45_662.0));
}
#[test]
fn format_renders_the_documented_pictures() {
assert_eq!(
ok("Format(4523.5, \"Fixed\")"),
FieldValue::Text("4523.50".into())
);
assert_eq!(
ok("Format(0.5, \"Percent\")"),
FieldValue::Text("50.00%".into())
);
assert_eq!(
ok("Format(1234567.891, \"Standard\")"),
FieldValue::Text("1,234,567.89".into())
);
assert_eq!(
ok("Format(#2025-01-05 13:45#, \"yyyy-mm-dd hh:nn\")"),
FieldValue::Text("2025-01-05 13:45".into())
);
assert_eq!(
ok("Format(3.14159, \"0.00\")"),
FieldValue::Text("3.14".into())
);
assert_eq!(
ok("Format(1234.5, \"#,##0\")"),
FieldValue::Text("1,235".into())
);
assert_eq!(
ok("Format(Null, \"Fixed\")"),
FieldValue::Text(String::new())
);
}
#[test]
fn math_functions_round_like_vba() {
assert_eq!(ok("Abs(-5)"), FieldValue::Integer(5));
assert_eq!(ok("Abs(-5.5)"), FieldValue::Float(5.5));
assert_eq!(ok("Int(5.9)"), FieldValue::Integer(5));
assert_eq!(ok("Int(-5.5)"), FieldValue::Integer(-6)); assert_eq!(ok("Fix(-5.5)"), FieldValue::Integer(-5)); assert_eq!(ok("Round(2.5)"), FieldValue::Float(2.0)); assert_eq!(ok("Round(3.5)"), FieldValue::Float(4.0));
assert_eq!(ok("Round(2.567, 2)"), FieldValue::Float(2.57));
assert_eq!(ok("Sqr(9)"), FieldValue::Float(3.0));
is_runtime_error("Sqr(-1)", "Sqr");
}
#[test]
fn malformed_sources_are_parse_errors() {
is_syntax_error("");
is_syntax_error("1 +");
is_syntax_error("(1");
is_syntax_error("\"open");
is_syntax_error("1 @ 2");
is_syntax_error("1 2");
}
#[test]
fn runtime_errors_are_precise() {
is_runtime_error("Foo(1)", "Foo");
is_runtime_error("Left(\"a\")", "Left");
is_runtime_error("Chr(70000)", "Chr");
is_runtime_error("Year(\"nope\")", "date");
}