From 531d2929300c8fdfaf6911d0b74f27d0c9229d1c Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Wed, 14 Oct 2026 11:46:48 +0200 Subject: [PATCH] N() converts a computed array element by element, so it works inside SUMPRODUCT (fixes #1279) --- crates/calc/src/tests.rs | 17 +++++++++++++++++ crates/functions/src/info.rs | 16 ++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 196c57de7..ab010e8c8 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -1266,3 +1266,20 @@ fn offset_extreme_arguments_return_ref() { assert_eq!(crate::evaluate(&wb, 0, c("D1"), formula), Value::Number(expected), "{formula}"); } } + +#[test] +fn n_lifts_over_a_computed_array_but_reads_the_first_cell_of_a_range() { + let mut t = T::new(); + t.set("B1", "0"); + t.set("B2", "23850"); + t.set("D2", "10%"); + t.set("D3", "12%"); + t.set("F1", "50000"); + t.set("H1", "=SUMPRODUCT(D2:D3-D1:D2,F1-B1:B2,N(F1>B1:B2))"); + assert!((t.num("H1") - 5523.0).abs() < 1e-9, "{:?}", t.get("H1")); + t.set("H2", "=SUMPRODUCT(N(B1:B2>0))"); + assert_eq!(t.num("H2"), 1.0); + // A spill of N(D2:D3) would also show 10% in H3; summing it tells the two apart. + t.set("H3", "=SUM(N(D2:D3))"); + assert!((t.num("H3") - 0.1).abs() < 1e-12, "{:?}", t.get("H3")); +} diff --git a/crates/functions/src/info.rs b/crates/functions/src/info.rs index 84ca72a48..7aa2c2660 100644 --- a/crates/functions/src/info.rs +++ b/crates/functions/src/info.rs @@ -89,7 +89,17 @@ fn na(_a: &[Arg], _c: &mut dyn Ctx) -> R { } fn n_fn(a: &[Arg], _c: &mut dyn Ctx) -> R { - match first(a) { + // N(range) reads the range's first cell, but a computed array (N(A1:A3>0), N({...})) is + // converted element by element, so N can stand in for `--` inside SUMPRODUCT. + if let Some(Arg { value: Value::Array(arr), from_ref: false }) = a.first() { + let data = arr.data.iter().map(|v| n_of(v.clone()).unwrap_or_else(Value::Error)).collect(); + return Array::new(arr.rows, arr.cols, data).map(Value::from).ok_or(CellError::Calc); + } + n_of(first(a)) +} + +fn n_of(v: Value) -> R { + match v { Value::Number(n) => Ok(Value::Number(n)), Value::Bool(b) => Ok(Value::Number(if b { 1.0 } else { 0.0 })), Value::Error(e) => Err(e), @@ -536,7 +546,9 @@ mod tests { close(ev("N", vec![b(true)]), 1.0); close(ev("N", vec![t("7")]), 0.0); is_err(ev("N", vec![e(CellError::Num)]), CellError::Num); - close(ev("N", vec![av(row(&[3.0, 4.0]))]), 3.0); + close(ev("N", vec![rf(row(&[3.0, 4.0]))]), 3.0); + let lifted = ev("N", vec![av(arr(vec![vec![Value::Bool(true), tv("x")], vec![Value::Error(CellError::Div0), nv(4.0)]]))]); + assert_eq!(rows_of(&lifted), vec![vec![nv(1.0), nv(0.0)], vec![Value::Error(CellError::Div0), nv(4.0)]]); is_text(ev("INFO", vec![t("RECALC")]), "Automatic"); is_err(ev("INFO", vec![t("bogus")]), CellError::Value); }