use crate::engine::convergence::values_converged;
use crate::engine::{CycleConfig, Engine, EvalConfig};
use crate::test_workbook::TestWorkbook;
use formualizer_common::{DateSystem, ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::parse;
const DS: DateSystem = DateSystem::Excel1900;
fn iterate_engine(max_iterations: u32, max_change: f64) -> Engine<TestWorkbook> {
Engine::new(
TestWorkbook::new(),
EvalConfig::default().with_cycle(CycleConfig::iterate(max_iterations, max_change)),
)
}
fn set_formula(engine: &mut Engine<TestWorkbook>, sheet: &str, row: u32, col: u32, f: &str) {
engine
.set_cell_formula(sheet, row, col, parse(f).expect("parse"))
.expect("set formula");
}
fn set_value(engine: &mut Engine<TestWorkbook>, sheet: &str, row: u32, col: u32, v: LiteralValue) {
engine
.set_cell_value(sheet, row, col, v)
.expect("set value");
}
fn err_kind(engine: &Engine<TestWorkbook>, sheet: &str, row: u32, col: u32) -> ExcelErrorKind {
match engine.get_cell_value(sheet, row, col) {
Some(LiteralValue::Error(e)) => e.kind,
other => panic!("expected error at {sheet} r{row}c{col}, got {other:?}"),
}
}
#[test]
fn comparator_inf_vs_inf_does_not_converge() {
let inf = LiteralValue::Number(f64::INFINITY);
let out = values_converged(&inf, &inf.clone(), 0.001, DS);
assert!(!out.converged, "inf vs inf must NOT converge (spec §6)");
assert!(
!out.nan_converged,
"the NaN flag is for NaN values, not inf"
);
assert!(!values_converged(&inf, &inf.clone(), f64::MAX, DS).converged);
}
#[test]
fn comparator_neg_inf_vs_neg_inf_does_not_converge() {
let ninf = LiteralValue::Number(f64::NEG_INFINITY);
assert!(!values_converged(&ninf, &ninf.clone(), 0.001, DS).converged);
}
#[test]
fn comparator_inf_vs_neg_inf_does_not_converge() {
let inf = LiteralValue::Number(f64::INFINITY);
let ninf = LiteralValue::Number(f64::NEG_INFINITY);
assert!(!values_converged(&inf, &ninf, f64::MAX, DS).converged);
assert!(!values_converged(&ninf, &inf, f64::MAX, DS).converged);
}
#[test]
fn comparator_inf_vs_finite_does_not_converge() {
let inf = LiteralValue::Number(f64::INFINITY);
let big = LiteralValue::Number(f64::MAX);
assert!(!values_converged(&big, &inf, f64::MAX, DS).converged);
assert!(!values_converged(&inf, &big, f64::MAX, DS).converged);
}
#[test]
fn comparator_f64_max_vs_f64_max_converges_with_zero_delta() {
let big = LiteralValue::Number(f64::MAX);
let out = values_converged(&big, &big.clone(), 0.001, DS);
assert!(out.converged);
assert_eq!(out.abs_delta, Some(0.0));
}
#[test]
fn operator_overflow_mid_iteration_stabilizes_as_num_error_and_converges() {
let mut engine = iterate_engine(100, 0.001);
set_formula(&mut engine, "Sheet1", 1, 2, "=C1*1E100"); set_formula(&mut engine, "Sheet1", 1, 3, "=B1*1E100+1"); engine.evaluate_all().unwrap();
assert_eq!(err_kind(&engine, "Sheet1", 1, 2), ExcelErrorKind::Num);
assert_eq!(err_kind(&engine, "Sheet1", 1, 3), ExcelErrorKind::Num);
let t = engine.last_cycle_telemetry();
assert_eq!(t.iterated_sccs, 1);
assert_eq!(t.converged_sccs, 1, "same-kind errors converge (§6)");
assert_eq!(t.capped_sccs, 0);
assert_eq!(t.circ_cells_stamped, 0, "no #CIRC is synthesized");
assert!(
t.settle_passes_total <= 6,
"overflow → error → convergence must be quick, got {} passes",
t.settle_passes_total
);
}
#[test]
fn aggregate_overflow_sanitizes_to_num_error_and_scc_converges() {
let mut engine = iterate_engine(7, 0.001);
set_value(&mut engine, "Sheet1", 1, 1, LiteralValue::Number(1.0e308));
set_value(&mut engine, "Sheet1", 2, 1, LiteralValue::Number(1.0e308));
set_formula(&mut engine, "Sheet1", 1, 2, "=MAX(SUM(A1:A2),C1)"); set_formula(&mut engine, "Sheet1", 1, 3, "=B1"); engine.evaluate_all().unwrap();
assert_eq!(err_kind(&engine, "Sheet1", 1, 2), ExcelErrorKind::Num);
assert_eq!(err_kind(&engine, "Sheet1", 1, 3), ExcelErrorKind::Num);
let t = engine.last_cycle_telemetry();
assert_eq!(t.iterated_sccs, 1);
assert_eq!(t.converged_sccs, 1, "same-kind errors converge (§6)");
assert_eq!(t.capped_sccs, 0, "no longer pinned at the pass-budget cap");
assert!(
t.settle_passes_total <= 6,
"overflow → error → convergence must be quick, got {} passes",
t.settle_passes_total
);
engine.evaluate_all().unwrap();
let t = engine.last_cycle_telemetry();
assert_eq!(t.converged_sccs, 1);
assert_eq!(t.capped_sccs, 0);
assert!(t.settle_passes_total <= 6);
}
#[test]
fn acyclic_aggregate_overflow_is_num_error_excel_parity() {
let mut engine = iterate_engine(100, 0.001);
set_value(&mut engine, "Sheet1", 1, 1, LiteralValue::Number(1.0e308));
set_value(&mut engine, "Sheet1", 2, 1, LiteralValue::Number(1.0e308));
set_formula(&mut engine, "Sheet1", 1, 2, "=SUM(A1:A2)"); set_formula(&mut engine, "Sheet1", 2, 2, "=AVERAGE(A1:A2)*2"); set_formula(&mut engine, "Sheet1", 3, 2, "=MAX(B1,0)"); set_formula(&mut engine, "Sheet1", 4, 2, "=SUMIF(A1:A2,\">0\")"); set_formula(&mut engine, "Sheet1", 5, 2, "=SUBTOTAL(9,A1:A2)"); engine.evaluate_all().unwrap();
assert_eq!(err_kind(&engine, "Sheet1", 1, 2), ExcelErrorKind::Num);
assert_eq!(err_kind(&engine, "Sheet1", 2, 2), ExcelErrorKind::Num);
assert_eq!(err_kind(&engine, "Sheet1", 3, 2), ExcelErrorKind::Num);
assert_eq!(err_kind(&engine, "Sheet1", 4, 2), ExcelErrorKind::Num);
assert_eq!(err_kind(&engine, "Sheet1", 5, 2), ExcelErrorKind::Num);
}
#[test]
fn sign_oscillating_infinity_caps_deterministically() {
let mut engine = iterate_engine(5, 0.001);
set_value(&mut engine, "Sheet1", 1, 1, LiteralValue::Number(1.0e308));
set_value(&mut engine, "Sheet1", 2, 1, LiteralValue::Number(1.0e308));
set_formula(&mut engine, "Sheet1", 1, 2, "=-C1"); set_formula(&mut engine, "Sheet1", 1, 3, "=MAX(B1,SUM(A1:A2))"); engine.evaluate_all().unwrap();
assert_eq!(err_kind(&engine, "Sheet1", 1, 2), ExcelErrorKind::Num);
assert_eq!(err_kind(&engine, "Sheet1", 1, 3), ExcelErrorKind::Num);
let t = engine.last_cycle_telemetry();
assert_eq!(t.iterated_sccs, 1);
assert_eq!(
t.converged_sccs, 1,
"once both members are #NUM!, error convergence applies"
);
}
#[test]
fn near_overflow_divergent_growth_caps_without_error() {
let mut engine = iterate_engine(50, 0.001);
set_formula(&mut engine, "Sheet1", 1, 2, "=C1*10"); set_formula(&mut engine, "Sheet1", 1, 3, "=B1*10+1"); engine.evaluate_all().unwrap();
let t = engine.last_cycle_telemetry();
assert_eq!(t.capped_sccs, 1);
assert_eq!(t.converged_sccs, 0);
let b = match engine.get_cell_value("Sheet1", 1, 2) {
Some(LiteralValue::Number(n)) => n,
other => panic!("expected number, got {other:?}"),
};
assert!(b.is_finite() && b > 1e90, "finite huge growth, got {b}");
}