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)); +}