XLS: array formulas fill their range on recalculation

A legacy array formula in a .xls file ({=FREQUENCY(...)}, {=TREND(...)}) is
stored as one ARRAY record plus a FORMULA record per cell of its range, each
with the cached result. The importer attached the formula to the anchor cell
but kept the cached values in the other cells as plain constants. On
recalculation the formula then found its range occupied and returned #SPILL!.
The .xlsx reader already drops those cached results; the .xls reader now does
the same once the formula is attached, and keeps the values when the formula
could not be imported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
neilcawse 2026-10-11 21:12:40 -04:00
parent fc45b7f6d8
commit 317ec4388c
3 changed files with 17 additions and 5 deletions

View File

@ -330,11 +330,12 @@ fn reads_values_formulas_formats_and_layout() {
// Shared formula: relative offsets resolve per cell.
assert_eq!(ftext(&wb, "B5").as_deref(), Some("A5*10"));
assert_eq!(ftext(&wb, "B6").as_deref(), Some("A6*10"));
// Array formula: the anchor holds it, the other cell keeps its value.
// Array formula: the anchor holds it; the other cell's cached result is dropped so that
// the formula fills the range on recalculation (as when an .xlsx is read, #156).
let c5 = wb.sheets[0].cell(CellRef::parse("C5").unwrap()).unwrap().formula.clone().unwrap();
assert_eq!((c5.text.as_str(), c5.array), ("A5:A6*2", Some(RangeRef::parse("C5:C6").unwrap())));
assert_eq!(ftext(&wb, "C6"), None);
assert_eq!(v(&wb, 0, "C6"), Value::Number(4.0));
assert_eq!(v(&wb, 0, "C6"), Value::Empty);
// Number formats, merges, sizes, names, print area; embedded chart records ignored.
let sh = &wb.sheets[0];
assert_eq!(wb.styles.get(sh.style_id(CellRef::parse("A4").unwrap())).num_fmt.as_str(), "yyyy-mm-dd");

View File

@ -507,9 +507,9 @@ fn worksheet(
}
// Shared and array formulas: their definitions follow the first cell that uses them.
let (arrays, shared) = (std::mem::take(&mut st.arrays), std::mem::take(&mut st.shared));
for p in std::mem::take(&mut st.pending) {
let pending = std::mem::take(&mut st.pending);
for p in &pending {
if let Some((range, f)) = arrays.get(&p.anchor) {
// The other cells of an array formula keep only their values.
if p.pos == p.anchor {
st.attach(p.pos, f, Mode::Cell, Some(*range));
}
@ -519,6 +519,16 @@ fn worksheet(
st.unsupported("a shared formula that is missing from the file");
}
}
// The other cells of a legacy array formula hold only cached results: dropped, as when an
// .xlsx is read, so that the formula fills its range on recalculation. If the formula itself
// couldn't be imported, they keep their values.
for p in &pending {
let filled = st.sheet.cell(p.anchor).and_then(|c| c.formula.as_ref()).and_then(|f| f.array).is_some_and(|r| r.contains(p.pos));
if p.pos != p.anchor && filled {
let style = st.sheet.style_id(p.pos);
st.sheet.cells.set(p.pos, Cell { value: Value::Empty, formula: None, style });
}
}
if st.dropped > 0 {
report.warn(format!("{} cells outside the grid were skipped.", st.dropped));
}

View File

@ -1,6 +1,6 @@
# File-format parity
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (CSV save keeps the separator it was opened with) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (`.xls` array formulas fill their range on recalculation) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
Every format Excel opens or saves, with GridCraft's read and write support, fidelity and tests.
Part of [target-app-parity.md](target-app-parity.md). **File formats: ~50% ready, 80–120 h**
@ -78,6 +78,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here.
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | minor | `.xls` import: the other cells of an array formula ({=FREQUENCY(...)}, {=TREND(...)}) drop their cached results, as `.xlsx` import does, so the formula fills its range on recalculation instead of showing `#SPILL!` against its own old values; 79 fewer mismatching formulas and 16 fewer round-trip differences over 4,224 real-world files |
| 2026-10-12 | minor | CSV: saving a file opened as CSV keeps its detected separator (`;` files were rewritten with `,`) |
| 2026-10-11 | minor | XLSX: cell/shared/array formulas read through the file→model conversion (`_xlpm.` LET/LAMBDA parameter prefixes stripped); table column names keep line breaks; table names may start with `\` |
| 2026-10-11 | minor | CSV/TSV export writes displayed values (number formats applied, no `#####`); `.xltx`, `.xltm` and `.xlsm` saved with their own workbook content types |