formualizer-eval 0.5.3

High-performance Arrow-backed Excel formula engine with dependency graph and incremental recalculation
Documentation
use crate::engine::named_range::{NameScope, NamedDefinition};
use crate::engine::{ChangeLog, Engine, EvalConfig};
use crate::test_workbook::TestWorkbook;
use formualizer_common::{ExcelErrorKind, LiteralValue};
use formualizer_parse::ASTNode;
use formualizer_parse::parser::parse as parse_formula;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;

fn parse(formula: &str) -> ASTNode {
    parse_formula(formula).expect("valid formula")
}

fn telemetry_config() -> EvalConfig {
    EvalConfig::default().with_virtual_dep_telemetry(true)
}

#[test]
fn indirect_simple_lookup_and_update() {
    let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());

    engine
        .set_cell_value("Sheet1", 1, 1, LiteralValue::Number(42.0))
        .unwrap();
    engine
        .set_cell_formula("Sheet1", 1, 2, parse("=INDIRECT(\"A1\")"))
        .unwrap();

    engine.evaluate_all().unwrap();
    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 2),
        Some(LiteralValue::Number(42.0))
    );

    engine
        .set_cell_value("Sheet1", 1, 1, LiteralValue::Number(100.0))
        .unwrap();
    engine.evaluate_all().unwrap();
    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 2),
        Some(LiteralValue::Number(100.0))
    );
}

#[test]
fn indirect_retarget_via_ref_text_edit() {
    let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());

    engine
        .set_cell_value("Sheet1", 1, 1, LiteralValue::Number(10.0))
        .unwrap();
    engine
        .set_cell_value("Sheet1", 2, 1, LiteralValue::Number(20.0))
        .unwrap();
    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A1".to_string()))
        .unwrap();
    engine
        .set_cell_formula("Sheet1", 1, 3, parse("=INDIRECT(B1)"))
        .unwrap();

    engine.evaluate_all().unwrap();
    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 3),
        Some(LiteralValue::Number(10.0))
    );

    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A2".to_string()))
        .unwrap();
    engine.evaluate_all().unwrap();
    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 3),
        Some(LiteralValue::Number(20.0))
    );
}

#[test]
fn indirect_invalid_ref_returns_ref_error() {
    let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());

    engine
        .set_cell_formula("Sheet1", 1, 1, parse("=INDIRECT(\"NOT_A_REF\")"))
        .unwrap();

    engine.evaluate_all().unwrap();

    match engine.get_cell_value("Sheet1", 1, 1) {
        Some(LiteralValue::Error(err)) => assert_eq!(err.kind, ExcelErrorKind::Ref),
        other => panic!("expected #REF! error, got {other:?}"),
    }
}

#[test]
fn indirect_retarget_parity_across_full_recalc_entrypoints() {
    let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());

    engine
        .set_cell_value("Sheet1", 1, 1, LiteralValue::Number(10.0))
        .unwrap();
    engine
        .set_cell_value("Sheet1", 2, 1, LiteralValue::Number(20.0))
        .unwrap();
    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A1".to_string()))
        .unwrap();
    engine
        .set_cell_formula("Sheet1", 1, 3, parse("=INDIRECT(B1)"))
        .unwrap();

    engine.evaluate_all().unwrap();

    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A2".to_string()))
        .unwrap();
    let (_res, _delta) = engine.evaluate_all_with_delta().unwrap();
    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 3),
        Some(LiteralValue::Number(20.0))
    );

    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A1".to_string()))
        .unwrap();
    let cancel = Arc::new(AtomicBool::new(false));
    engine.evaluate_all_cancellable(cancel).unwrap();
    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 3),
        Some(LiteralValue::Number(10.0))
    );

    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A2".to_string()))
        .unwrap();
    let mut log = ChangeLog::new();
    engine.evaluate_all_logged(&mut log).unwrap();
    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 3),
        Some(LiteralValue::Number(20.0))
    );
}

#[test]
fn indirect_supports_quoted_sheet_and_ranges() {
    let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());

    engine
        .set_cell_value("Data Sheet", 1, 1, LiteralValue::Number(2.0))
        .unwrap();
    engine
        .set_cell_value("Data Sheet", 2, 1, LiteralValue::Number(3.0))
        .unwrap();
    engine
        .set_cell_value("Data Sheet", 3, 1, LiteralValue::Number(5.0))
        .unwrap();

    engine
        .set_cell_formula("Sheet1", 1, 1, parse("=INDIRECT(\"'Data Sheet'!A1\")"))
        .unwrap();
    engine
        .set_cell_formula(
            "Sheet1",
            1,
            2,
            parse("=SUM(INDIRECT(\"'Data Sheet'!A1:A3\"))"),
        )
        .unwrap();

    engine.evaluate_all().unwrap();

    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 1),
        Some(LiteralValue::Number(2.0))
    );
    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 2),
        Some(LiteralValue::Number(10.0))
    );
}

#[test]
fn indirect_supports_named_ranges_and_maps_missing_name_to_ref() {
    let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());

    engine
        .define_name(
            "MyValue",
            NamedDefinition::Literal(LiteralValue::Number(77.0)),
            NameScope::Workbook,
        )
        .unwrap();

    engine
        .set_cell_formula("Sheet1", 1, 1, parse("=INDIRECT(\"MyValue\")"))
        .unwrap();
    engine
        .set_cell_formula("Sheet1", 1, 2, parse("=INDIRECT(\"UnknownName\")"))
        .unwrap();

    engine.evaluate_all().unwrap();

    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 1),
        Some(LiteralValue::Number(77.0))
    );
    match engine.get_cell_value("Sheet1", 1, 2) {
        Some(LiteralValue::Error(err)) => assert_eq!(err.kind, ExcelErrorKind::Ref),
        other => panic!("expected #REF! error, got {other:?}"),
    }
}

#[test]
fn indirect_a1_false_returns_not_implemented_error() {
    let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());

    engine
        .set_cell_formula("Sheet1", 1, 1, parse("=INDIRECT(\"R1C1\",FALSE)"))
        .unwrap();

    engine.evaluate_all().unwrap();

    match engine.get_cell_value("Sheet1", 1, 1) {
        Some(LiteralValue::Error(err)) => assert_eq!(err.kind, ExcelErrorKind::NImpl),
        other => panic!("expected #NIMPL error, got {other:?}"),
    }
}

#[test]
fn recalc_plan_with_indirect_falls_back_to_dynamic_recalc() {
    let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());

    engine
        .set_cell_value("Sheet1", 1, 1, LiteralValue::Number(10.0))
        .unwrap();
    engine
        .set_cell_value("Sheet1", 2, 1, LiteralValue::Number(20.0))
        .unwrap();
    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A1".to_string()))
        .unwrap();
    engine
        .set_cell_formula("Sheet1", 1, 3, parse("=INDIRECT(B1)"))
        .unwrap();

    let plan = engine.build_recalc_plan().unwrap();
    assert!(plan.has_dynamic_refs());

    engine.evaluate_all().unwrap();
    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A2".to_string()))
        .unwrap();

    let before = engine.virtual_dep_fallback_activations();
    engine.evaluate_recalc_plan(&plan).unwrap();
    let after = engine.virtual_dep_fallback_activations();

    assert_eq!(after, before + 1);
    assert_eq!(
        engine.get_cell_value("Sheet1", 1, 3),
        Some(LiteralValue::Number(20.0))
    );
}

#[test]
fn virtual_dep_telemetry_reports_convergence_for_indirect_retarget() {
    let mut engine = Engine::new(TestWorkbook::new(), telemetry_config());

    engine
        .set_cell_value("Sheet1", 1, 1, LiteralValue::Number(10.0))
        .unwrap();
    engine
        .set_cell_value("Sheet1", 2, 1, LiteralValue::Number(20.0))
        .unwrap();
    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A1".to_string()))
        .unwrap();
    engine
        .set_cell_formula("Sheet1", 1, 3, parse("=INDIRECT(B1)"))
        .unwrap();

    engine.evaluate_all().unwrap();
    engine
        .set_cell_value("Sheet1", 1, 2, LiteralValue::Text("A2".to_string()))
        .unwrap();
    engine.evaluate_all().unwrap();

    let t = engine.last_virtual_dep_telemetry();
    assert!(t.candidate_vertices_total > 0);
    assert!(t.schedule_virtual_passes > 0 || t.schedule_static_passes > 0);
    assert_eq!(t.bailout_reason, Some("converged"));
}

#[test]
fn virtual_dep_telemetry_static_workbook_has_no_dynamic_changes() {
    let mut engine = Engine::new(TestWorkbook::new(), telemetry_config());

    engine
        .set_cell_value("Sheet1", 1, 1, LiteralValue::Number(5.0))
        .unwrap();
    engine
        .set_cell_formula("Sheet1", 1, 2, parse("=A1+1"))
        .unwrap();

    engine.evaluate_all().unwrap();
    let t = engine.last_virtual_dep_telemetry();
    assert!(t.candidate_vertices_total > 0);
    assert_eq!(t.changed_vdeps_total, 0);
}

#[test]
fn virtual_dep_telemetry_disabled_by_default() {
    let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());

    engine
        .set_cell_value("Sheet1", 1, 1, LiteralValue::Number(5.0))
        .unwrap();
    engine
        .set_cell_formula("Sheet1", 1, 2, parse("=A1+1"))
        .unwrap();

    engine.evaluate_all().unwrap();
    let t = engine.last_virtual_dep_telemetry();
    assert_eq!(t.candidate_vertices_total, 0);
    assert_eq!(t.schedule_virtual_passes + t.schedule_static_passes, 0);
}