From faa0ff9b69fb5d65dac0b2827e533d69fb1e32d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Thu, 31 Jul 2025 18:24:48 +0200 Subject: [PATCH] FIX: Minimal support for inlineStr Fixes #424 --- xlsx/src/import/worksheets.rs | 50 +++++++++++++++++++++---------- xlsx/tests/openpyxl_example.xlsx | Bin 0 -> 4879 bytes xlsx/tests/test.rs | 30 +++++++++++++++++++ 3 files changed, 65 insertions(+), 15 deletions(-) create mode 100644 xlsx/tests/openpyxl_example.xlsx diff --git a/xlsx/src/import/worksheets.rs b/xlsx/src/import/worksheets.rs index f3e0bd9..cb946f0 100644 --- a/xlsx/src/import/worksheets.rs +++ b/xlsx/src/import/worksheets.rs @@ -336,6 +336,7 @@ fn get_cell_from_excel( sheet_name: &str, cell_ref: &str, shared_strings: &mut Vec, + rich_text_inline: Option, ) -> Cell { // Possible cell types: // 18.18.11 ST_CellType (Cell Type) @@ -397,12 +398,15 @@ fn get_cell_from_excel( } } "inlineStr" => { - // Not implemented - println!("Invalid type (inlineStr) in {sheet_name}!{cell_ref}"); - Cell::ErrorCell { - ei: Error::NIMPL, - s: cell_style, - } + let s = rich_text_inline.unwrap_or_default(); + let si = if let Some(i) = shared_strings.iter().position(|r| r == &s) { + i + } else { + shared_strings.push(s.to_string()); + shared_strings.len() - 1 + } as i32; + + Cell::SharedString { si, s: cell_style } } "empty" => Cell::EmptyCell { s: cell_style }, _ => { @@ -480,16 +484,11 @@ fn get_cell_from_excel( } } "inlineStr" => { - // Not implemented - let o = format!("{sheet_name}!{cell_ref}"); - let m = Error::NIMPL.to_string(); - println!("Invalid type (inlineStr) in {sheet_name}!{cell_ref}"); - Cell::CellFormulaError { + // NB: This is untested, I don't know of any engine that uses inline strings in formulas + Cell::CellFormulaString { f: formula_index, - ei: Error::NIMPL, + v: rich_text_inline.unwrap_or("".to_string()), s: cell_style, - o, - m, } } _ => { @@ -796,7 +795,7 @@ pub(super) fn load_sheet( // 18.3.1.4 c (Cell) // Child Elements: // * v: Cell value - // * is: Rich Text Inline (not used in IronCalc) + // * is: Rich Text Inline // * f: Formula // Attributes: // r: reference. A1 style @@ -820,6 +819,26 @@ pub(super) fn load_sheet( None }; + // + // + // Hello, World! + // + // + let cell_rich_text_nodes: Vec = + cell.children().filter(|n| n.has_tag_name("is")).collect(); + let cell_rich_text = if cell_rich_text_nodes.is_empty() { + None + } else { + let texts: Vec = cell_rich_text_nodes[0] + .descendants() + .filter(|n| n.has_tag_name("t")) + .filter_map(|n| n.text()) + .map(|s| s.to_string()) + .collect(); + + Some(texts.join("")) + }; + let cell_metadata = cell.attribute("cm"); // type, the default type being "n" for number @@ -976,6 +995,7 @@ pub(super) fn load_sheet( sheet_name, cell_ref, shared_strings, + cell_rich_text, ); data_row.insert(column, cell); } diff --git a/xlsx/tests/openpyxl_example.xlsx b/xlsx/tests/openpyxl_example.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..aa1609e0062a604861f8b28124612c64ced3451a GIT binary patch literal 4879 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%0Aup8H491blsr{TU3B6VXG4yTzb6!-MqkB&95r} z%kckiWn4sG>>K>X0ssLx?f*poUl-vb{Nm~Q4}1pu`F}ZUUlee$y85qxKvv>k0{*qu zx+vvh=Krr0oNkg|QvTEd7oiv9_8;g?^1q_^Mc~Ca_y;J2Ek3ZS|I=kJf-m~{A8-Wa qFYy2H`ipWdy3!vxP;6}e7st}oCcrjB000u~t%!}_O+Wixfd2rVwuX%W literal 0 HcmV?d00001 diff --git a/xlsx/tests/test.rs b/xlsx/tests/test.rs index f86ec91..da4bdaa 100644 --- a/xlsx/tests/test.rs +++ b/xlsx/tests/test.rs @@ -531,3 +531,33 @@ fn test_user_model() { // we can still use the model afterwards model.set_rows_height(0, 1, 1, 100.0).unwrap(); } + +// This is produced with: +// from openpyxl import Workbook + +// # Create new workbook +// wb = Workbook() +// ws = wb.active + +// # Write text and formula +// ws['A1'] = 'Hello, World!' +// ws['A2'] = '=1+1' + +// ws['B1'] = '=CONCAT("It is", " what it is")' + +// # Save +// wb.save('openpyxl_example.xlsx') +#[test] +fn test_pyopenxl_example() { + let mut model = load_from_xlsx("tests/openpyxl_example.xlsx", "en", "UTC").unwrap(); + model.evaluate(); + + let a1 = model.get_formatted_cell_value(0, 1, 1).unwrap(); + assert_eq!(a1, "Hello, World!"); + + let a2 = model.get_formatted_cell_value(0, 2, 1).unwrap(); + assert_eq!(a2, "2"); + + let b1 = model.get_formatted_cell_value(0, 1, 2).unwrap(); + assert_eq!(b1, "It is what it is"); +}