Ranges keep their size past the used range

A multi-cell reference was read only up to the sheet's used range, so
the array it gave depended on what else was on the sheet:
ROWS(A1:A10*1), COUNTBLANK(A1:A10) and SUMPRODUCT(--(A1:A10="")) came
out smaller than in Excel, and changed as cells were entered elsewhere
without the formula recalculating. References of up to 16,384 cells now
keep their size; larger ones (A:A, ranges reserved for data to come
such as $A$2:$A$50000) still stop at the used range, so that formulas
over them stay fast.
This commit is contained in:
nihilist-narwhal
2026-10-10 03:19:45 -05:00
parent 13664fe66b
commit 62b17379e3
2 changed files with 26 additions and 1 deletions
+8 -1
View File
@@ -13,6 +13,10 @@ use gridcraft_model::Workbook;
/// Largest array a reference may expand to.
pub const MAX_CELLS: u64 = 16_000_000;
/// References up to this many cells are read at their full size; larger ones (`A:A`, ranges
/// reserved for data to come such as `$A$2:$A$50000`) stop at the sheet's used range, so that
/// formulas over them stay fast.
const FULL_SIZE_CELLS: u64 = 16_384;
const MAX_DEPTH: usize = 400;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@@ -129,7 +133,10 @@ impl<'h> Evaluator<'h> {
if a.range.is_single() {
return self.host.cell_value(a.sheet, a.range.start);
}
let range = self.trim(a);
// A reference keeps its size, so the array has its shape and its blanks count
// (`=A1:A10`, `SUMPRODUCT(--(A1:A10=""))`).
let cells = u64::from(a.range.height()) * u64::from(a.range.width());
let range = if cells > FULL_SIZE_CELLS { self.trim(a) } else { a.range };
let (h, w) = (range.height() as usize, range.width() as usize);
if (h as u64) * (w as u64) > MAX_CELLS {
return Value::Error(CellError::Num);
+18
View File
@@ -158,6 +158,24 @@ fn spills() {
assert_eq!(t.num("D2"), 40.0);
}
#[test]
fn references_keep_their_size_past_the_used_range() {
let mut t = T::new();
t.set("A1", "5");
t.set("A2", "6");
t.set("C1", "=ROWS(A1:A10*1)");
t.set("C2", "=SUMPRODUCT(--(A1:A10=\"\"))");
t.set("C3", "=COUNTBLANK(A1:A10)");
t.set("C4", "=COUNTIF(A1:A10,\"\")");
t.set("C5", "=COLUMNS(TRANSPOSE(A1:A10))");
for (at, want) in [("C1", 10.0), ("C2", 8.0), ("C3", 8.0), ("C4", 8.0), ("C5", 10.0)] {
assert_eq!(t.num(at), want, "{at}");
}
// Whole columns are still read up to the used range.
t.set("D1", "=ROWS(FILTER(A:A,A:A<>\"\"))");
assert_eq!(t.num("D1"), 2.0);
}
#[test]
fn let_lambda() {
let mut t = T::new();