From 9a58f2d086ae56d4b254bf101e48388815cb2b46 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 22:40:01 +0200 Subject: [PATCH] XLS: a formula kept as its saved value keeps a text result (fixes #802) --- crates/xlsx/src/tests/xls_tests.rs | 15 +++++++++++++++ crates/xlsx/src/xls/mod.rs | 10 +++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/xlsx/src/tests/xls_tests.rs b/crates/xlsx/src/tests/xls_tests.rs index fd37795..f008883 100644 --- a/crates/xlsx/src/tests/xls_tests.rs +++ b/crates/xlsx/src/tests/xls_tests.rs @@ -358,6 +358,21 @@ fn reads_values_formulas_formats_and_layout() { assert_eq!(wb.date_system, DateSystem::D1904); } +#[test] +fn text_result_of_a_formula_kept_as_its_value() { + // A what-if data table cell ({=TABLE(B1,)}) can't be imported as a formula, so its saved result + // is kept. With the default XF the cell is blank until the String record brings the text. + let table = cat(&[&[0x02], &u16s(&[0, 0])]); + let sheet = cat(&[&bof(0x0010), &formula(0, 0, special(0, 0), &table), &rec(0x0207, &xlstr("103a")), &eof()]); + let boundsheet = |pos: u32| rec(0x0085, &cat(&[&pos.to_le_bytes(), &[0, 0, 6, 0], b"Sheet1"])); + let len = cat(&[&bof(0x0005), &boundsheet(0), &eof()]).len() as u32; + let stream = cat(&[&bof(0x0005), &boundsheet(len), &eof(), &sheet]); + let (wb, report) = read_xls(&compound("Workbook", &stream)).unwrap(); + assert!(report.warnings.iter().any(|w| w.contains("what-if data tables")), "{:?}", report.warnings); + assert_eq!(ftext(&wb, "A1"), None); + assert_eq!(v(&wb, 0, "A1"), Value::from("103a")); +} + #[test] fn refuses_encrypted_old_and_foreign_files() { // FILEPASS in the globals: an encrypted workbook. diff --git a/crates/xlsx/src/xls/mod.rs b/crates/xlsx/src/xls/mod.rs index c9aee66..0e9fb64 100644 --- a/crates/xlsx/src/xls/mod.rs +++ b/crates/xlsx/src/xls/mod.rs @@ -390,7 +390,9 @@ struct SheetState<'g> { shared: HashMap, arrays: HashMap, last_formula: Option, - pending_string: Option, + /// The cell waiting for its String record, with its style: a blank cell isn't stored, so a + /// formula kept as its value may have no cell to receive the text. + pending_string: Option<(CellRef, StyleId)>, dropped: usize, unsupported: Vec<&'static str>, } @@ -594,11 +596,13 @@ fn sheet_record(rec: &Record<'_>, st: &mut SheetState<'_>, budget: &mut Budget, } 0x0006 => formula_cell(&mut c, st, budget)?, 0x0207 => { - if let Some(pos) = st.pending_string.take() { + if let Some((pos, style)) = st.pending_string.take() { let s = c.string()?; budget.text(s.len())?; if let Some(cell) = st.sheet.cells.get_mut(pos) { cell.value = Value::from(s); + } else { + st.sheet.cells.set(pos, Cell { value: Value::from(s), formula: None, style }); } } } @@ -736,7 +740,7 @@ fn formula_cell(c: &mut Cursor<'_, '_>, st: &mut SheetState<'_>, budget: &mut Bu st.sheet.cells.set(pos, Cell { value, formula: None, style }); st.last_formula = Some(pos); if string_result { - st.pending_string = Some(pos); + st.pending_string = Some((pos, style)); } // PtgExp: part of a shared or array formula anchored at another cell. if f.rgce.len() == 5 && f.rgce.first() == Some(&0x01) {