mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-26 03:36:54 -06:00
Merge PR #1568: calc: bounded references up to 1,048,576 cells keep their full shape
This commit is contained in:
+19
-6
@@ -13,10 +13,14 @@ 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.
|
||||
/// References up to this many cells (one whole row) are always read at their full size.
|
||||
const FULL_SIZE_CELLS: u64 = 16_384;
|
||||
/// Bounded references up to this many cells (one column's worth) are read at their full size too, as
|
||||
/// Excel does, so their shape lines up with the arrays next to them (`D3:AZ400*B3:B400` is 398 by
|
||||
/// 49 even when the data ends at column F) and their blanks count. References that run to the edge
|
||||
/// of the sheet (`A:A`, `A2:A1048576`, several whole rows) and larger blocks stop at the sheet's used
|
||||
/// range, so that formulas over them stay fast.
|
||||
const FULL_SIZE_BOUNDED_CELLS: u64 = 1_048_576;
|
||||
const MAX_DEPTH: usize = 400;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -154,8 +158,13 @@ impl<'h> Evaluator<'h> {
|
||||
Ok(self.resolve_sheet(&r.sheet)?.into_iter().map(|sheet| Area { sheet, range }).collect())
|
||||
}
|
||||
|
||||
/// Dereferences: one cell → its value; a block → an array (trimmed to the used range);
|
||||
/// several areas → `#VALUE!`.
|
||||
/// Dereferences: one cell → its value; a block → an array; several areas → `#VALUE!`.
|
||||
///
|
||||
/// A block keeps its full shape (blanks included) when it holds at most `FULL_SIZE_CELLS`
|
||||
/// cells, or when it holds at most `FULL_SIZE_BOUNDED_CELLS` cells (one full column's worth)
|
||||
/// and does not run to the sheet's last row or column. Every other block (larger, or a
|
||||
/// bigger one that runs to the sheet's edge, such as `A:A` or `1:1`) is trimmed to the
|
||||
/// sheet's used range before its values are read. An array over `MAX_CELLS` is `#NUM!`.
|
||||
pub fn deref(&mut self, ev: Ev) -> Value {
|
||||
match ev {
|
||||
Ev::V(v) => v,
|
||||
@@ -174,7 +183,11 @@ impl<'h> Evaluator<'h> {
|
||||
// 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 };
|
||||
// `A:A`, `1:1` and `A2:A1048576` run to the edge of the sheet: they stand for "the rest of
|
||||
// the column", not for a block of a chosen size.
|
||||
let to_edge = a.range.end.row == MAX_ROWS - 1 || a.range.end.col == MAX_COLS - 1;
|
||||
let full_size = cells <= FULL_SIZE_CELLS || (!to_edge && cells <= FULL_SIZE_BOUNDED_CELLS);
|
||||
let range = if full_size { a.range } else { self.trim(a) };
|
||||
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);
|
||||
|
||||
@@ -1412,6 +1412,54 @@ fn deep_loop_is_found_where_it_closes() {
|
||||
assert_eq!(t.num("B1"), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_bounded_references_keep_their_shape() {
|
||||
// D3:AZ400 is 19,502 cells, past the size where a block used to be cut to the used range
|
||||
// (which ends at column F here). Sliced that way it no longer lined up with the full-size
|
||||
// operands next to it. Expected values observed in Excel 365 on the same layout.
|
||||
let mut t = T::new();
|
||||
for (at, v) in [
|
||||
("D1", "1"),
|
||||
("E1", "2"),
|
||||
("F1", "3"),
|
||||
("A3", "1"),
|
||||
("A4", "2"),
|
||||
("A5", "1"),
|
||||
("B3", "2"),
|
||||
("B4", "3"),
|
||||
("B5", "4"),
|
||||
("D3", "10"),
|
||||
("E3", "20"),
|
||||
("F3", "30"),
|
||||
("D4", "40"),
|
||||
("E4", "50"),
|
||||
("F4", "60"),
|
||||
("D5", "70"),
|
||||
("E5", "80"),
|
||||
("F5", "90"),
|
||||
] {
|
||||
t.set(at, v);
|
||||
}
|
||||
let cases = [
|
||||
("=SUMPRODUCT(D3:AZ400*B3:B400,(A3:A400=1)*(D1:AZ1=2))", Value::Number(360.0)),
|
||||
("=ROWS(D3:AZ400*1)", Value::Number(398.0)),
|
||||
("=COLUMNS(D3:AZ400*1)", Value::Number(49.0)),
|
||||
("=SUMPRODUCT(D3:AZ400,(A3:A400=1)*(D1:AZ1=2))", Value::Number(100.0)),
|
||||
("=SUMPRODUCT(D3:AZ400+0,D3:AZ401)", Value::Error(CellError::Value)),
|
||||
("=COUNTBLANK(D3:AZ400)", Value::Number(19493.0)),
|
||||
("=SUMPRODUCT(--(D3:AZ400=\"\"))", Value::Number(19493.0)),
|
||||
("=SUM(D3:AZ400*B3:B400)", Value::Number(1530.0)),
|
||||
("=COLUMNS(TRANSPOSE(D3:AZ400))", Value::Number(398.0)),
|
||||
// Whole columns are still read up to the used range.
|
||||
("=SUMPRODUCT((A:A=1)*B:B)", Value::Number(6.0)),
|
||||
];
|
||||
for (i, (formula, want)) in cases.iter().enumerate() {
|
||||
let at = format!("C{}", 1001 + i);
|
||||
t.set(&at, formula);
|
||||
assert_eq!(&t.get(&at), want, "{formula}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn median_large_values_recalculate_dependents() {
|
||||
let mut t = T::new();
|
||||
|
||||
@@ -91,7 +91,7 @@ PIVOTBY, REGEXTEST…) are all present.
|
||||
**~82% ready.** The library is broad and has 96 test functions in `crates/functions` with many
|
||||
assertions each, but there is no conformance suite against Excel-computed results, and user
|
||||
reports keep finding edge cases: MIN() with no arguments (#138), COUNTBLANK past the used range
|
||||
(#137), ROUND at 15 digits and DATE(1900,2,29) (fixed 10-09), SUMIF sum-range sizing (fixed
|
||||
(#137, and past 16,384 cells #1010, fixed 10-14 for bounded references), ROUND at 15 digits and DATE(1900,2,29) (fixed 10-09), SUMIF sum-range sizing (fixed
|
||||
10-09). Statistical distributions and financial functions with iterative solvers (RATE, IRR,
|
||||
XIRR, YIELD) are the highest-risk for last-digit differences.
|
||||
|
||||
@@ -116,6 +116,7 @@ workbooks; results checked in as data, not Excel files) and fixing what it finds
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | MEDIAN and AGGREGATE median use an overflow-safe midpoint (#1579); regressions cover finite extremes, tiny values and dependent recalculation |
|
||||
| 2026-10-13 | minor | Clear reports changed stored cells sparsely, including those beyond the first million coordinates; dependent formulas and removed spills update, while clearing empty merges still unblocks spills (#1032) |
|
||||
| 2026-10-14 | minor | Bounded references up to 1,048,576 cells (`D3:AZ400`) keep their full shape instead of stopping at the used range, so `SUMPRODUCT(D3:AZ400*B3:B400,(A3:A400=x)*(D1:AZ1=y))` broadcasts like Excel and `ROWS`, `COUNTBLANK` and `--(r="")` count the blanks; references that run to the edge of the sheet (`A:A`) still stop at the used range (#1010) |
|
||||
| 2026-10-12 | minor | USDOLLAR added as a Compatibility function (not on Microsoft's list, so the counts don't change); MIRR with no positive flow is -1, COUNTIF's bare `"<="` / `">="` skip empty strings, INDIRECT with `a1` = FALSE gives `#REF!` for A1-style text (#738) |
|
||||
| 2026-10-11 | minor | TEXT / number formats fixed against Excel-cached results: fraction literals and zero fractions, `0` denominators zero padded, `?` pads the thousands separator, booleans use the text section (226 mismatches → 0 in 352 TEXT cells) |
|
||||
| 2026-10-11 | minor | Iterative calculation: circular references iterate up to Maximum Iterations / Maximum Change when enabled |
|
||||
|
||||
Reference in New Issue
Block a user