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.
This commit is contained in:
Hamza
2026-10-19 20:38:30 +05:00
parent 66ce6be474
commit d890a1a7da
2 changed files with 43 additions and 3 deletions
+5 -3
View File
@@ -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),
+38
View File
@@ -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));
}