diff --git a/crates/xlsx/src/tests/xls_tests.rs b/crates/xlsx/src/tests/xls_tests.rs index fd37795..72829ce 100644 --- a/crates/xlsx/src/tests/xls_tests.rs +++ b/crates/xlsx/src/tests/xls_tests.rs @@ -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"); diff --git a/crates/xlsx/src/xls/mod.rs b/crates/xlsx/src/xls/mod.rs index c9aee66..e0c8cf3 100644 --- a/crates/xlsx/src/xls/mod.rs +++ b/crates/xlsx/src/xls/mod.rs @@ -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)); } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 8510659..c32fa47 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -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 |