From d890a1a7da6f9907992d2c1bb9d009d19387c013 Mon Sep 17 00:00:00 2001 From: Hamza Date: Thu, 15 Oct 2026 17:41:11 +0500 Subject: [PATCH] Calc: a dynamic array of one value still anchors a spill range (#1491) `C1#` gave `#REF!` while the dynamic-array formula in C1 happened to return a single value -- a FILTER matching one row, a SORT or UNIQUE over one item -- so `ROWS(C1#)` and `SUM(C1#)` failed and `COUNT(C1#)` gave 0. Every formula that counts or sums a filtered list therefore broke the moment the list had exactly one entry. In Excel the formula is the anchor of a 1 x 1 range and the three give 1, the value, and 1. A pass recorded the spill only for an array larger than one cell: if a.rows > 1 || a.cols > 1 { self.spills.insert(k, a.clone()); } so a 1 x 1 result left no range for `spill_range` to answer with, and `eval` turned the missing range into `#REF!`. It is recorded unconditionally now. The report suggested keying this on the cell carrying `cm`, which turned out not to be expressible: nothing in the model says "dynamic array" positively -- the reader tells one from a legacy CSE formula only by leaving `Formula::array` unset. The arm itself is the better test, because a formula whose result is a scalar never reaches it: `=1+1` does not become an anchor, which a test pins. It also matches Excel 365, where `=A1:A1` is a 1 x 1 anchor of its own. --- crates/calc/src/recalc.rs | 8 +++++--- crates/calc/src/tests.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index b391821d7..aa9da716f 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -517,9 +517,11 @@ impl PassHost<'_> { let data = a.data.iter().map(|x| if matches!(x, Value::Empty) { Value::Number(0.0) } else { x.clone() }).collect(); Array::new(a.rows, a.cols, data).map(std::sync::Arc::new).unwrap_or(a) }; - if a.rows > 1 || a.cols > 1 { - self.spills.insert(k, a.clone()); - } + // A dynamic-array formula anchors a spill range even while its result is one + // value -- a FILTER matching one row, a SORT or UNIQUE over one item -- so `C1#` + // is the 1 x 1 range and `ROWS(C1#)` is 1, as in Excel (#1491). A formula that + // returns a scalar never reaches this arm, so it does not become an anchor. + self.spills.insert(k, a.clone()); a.data.first().cloned().unwrap_or_default() } Value::Empty => Value::Number(0.0), diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 97f7a8350..15562b7c5 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -1294,3 +1294,41 @@ fn median_large_values_recalculate_dependents() { assert!((t.num(cell) + 1.3).abs() < 1e-14); } } + +#[test] +fn a_dynamic_array_of_one_value_still_anchors_a_spill() { + // In Excel a dynamic-array formula anchors a spill range even while its result is a single + // value, so `C1#` is the 1 x 1 range and ROWS/SUM/COUNT of it work. GridCraft gave #REF! + // (and COUNT 0), so every formula counting a filtered list broke the moment the list had + // exactly one entry (#1491). Values and cells follow the reported fixture. + let mut t = T::new(); + for i in 1..=5 { + t.set(&format!("A{i}"), &i.to_string()); + t.set(&format!("B{i}"), "7"); + } + t.set("C1", "=FILTER(A1:A5,A1:A5>4)"); + t.set("C3", "=SORT(FILTER(A1:A5,A1:A5=3))"); + t.set("C5", "=UNIQUE(B1:B5)"); + // Control: the same FILTER matching two rows, which worked before. + t.set("C9", "=FILTER(A1:A5,A1:A5>3)"); + + t.set("E1", "=ROWS(C1#)"); + t.set("E2", "=SUM(C1#)"); + t.set("E3", "=ROWS(C3#)"); + t.set("E5", "=COUNT(C5#)"); + t.set("E9", "=ROWS(C9#)"); + assert_eq!(t.get("E1"), Value::from(1.0)); + assert_eq!(t.get("E2"), Value::from(5.0)); + assert_eq!(t.get("E3"), Value::from(1.0)); + assert_eq!(t.get("E5"), Value::from(1.0)); + assert_eq!(t.get("E9"), Value::from(2.0), "a spilling array is unchanged"); + + // The anchors still show their first value, as they did. + assert_eq!(t.get("C1"), Value::from(5.0)); + assert_eq!(t.get("C5"), Value::from(7.0)); + + // A formula whose result is a scalar never becomes an anchor: `#` on it is still #REF!. + t.set("D1", "=1+1"); + t.set("E11", "=ROWS(D1#)"); + assert_eq!(t.get("E11"), Value::Error(CellError::Ref)); +}