From c121fb590152c562546dc2d248109b7e06195e0e Mon Sep 17 00:00:00 2001 From: Hector Villarreal Date: Tue, 3 Nov 2026 01:07:29 -0600 Subject: [PATCH] #2809: keep a workbook's R1C1 reference style through open and save A workbook saved by Excel in R1C1 reference style says so in . GridCraft didn't read the attribute, so opening and saving such a file switched it back to A1 style for the next person who opened it in Excel. The calculation settings now keep it (CalcSettings.r1c1, serde default false) and the writer puts it back; A1 stays the default and isn't written. GridCraft itself still shows and takes A1 references; the R1C1 display and entry are the rest of the issue. Co-Authored-By: Claude Opus 5.5 --- crates/model/src/workbook.rs | 5 +++++ crates/xlsx/src/read.rs | 1 + crates/xlsx/src/tests/roundtrip.rs | 20 +++++++++++++++++++- crates/xlsx/src/write.rs | 3 +++ docs/file-format-parity.md | 3 ++- 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/model/src/workbook.rs b/crates/model/src/workbook.rs index 2d16b46b5..1c5b007f6 100644 --- a/crates/model/src/workbook.rs +++ b/crates/model/src/workbook.rs @@ -95,6 +95,10 @@ pub struct CalcSettings { pub max_change: f64, /// Use precision as displayed. pub precision_as_displayed: bool, + /// R1C1 reference style (``): kept with the file and written back; + /// GridCraft itself shows and takes A1 references. + #[serde(default)] + pub r1c1: bool, } impl Default for CalcSettings { @@ -105,6 +109,7 @@ impl Default for CalcSettings { max_iterations: Self::DEFAULT_ITERATIONS, max_change: Self::DEFAULT_CHANGE, precision_as_displayed: false, + r1c1: false, } } } diff --git a/crates/xlsx/src/read.rs b/crates/xlsx/src/read.rs index 3cc2a6655..ead44c0e7 100644 --- a/crates/xlsx/src/read.rs +++ b/crates/xlsx/src/read.rs @@ -221,6 +221,7 @@ fn read_workbook_properties(cx: &mut Ctx<'_>, wb: &mut Workbook, xml: &El) { wb.calc.set_max_change(d); } wb.calc.precision_as_displayed = !c.flag("fullPrecision", true); + wb.calc.r1c1 = c.attr("refMode") == Some("R1C1"); } if let Some(p) = xml.child("workbookProtection") { wb.protected_structure = p.flag("lockStructure", false); diff --git a/crates/xlsx/src/tests/roundtrip.rs b/crates/xlsx/src/tests/roundtrip.rs index 2bdba21b8..ade0835c2 100644 --- a/crates/xlsx/src/tests/roundtrip.rs +++ b/crates/xlsx/src/tests/roundtrip.rs @@ -20,7 +20,7 @@ fn rr(a: &str) -> RangeRef { fn sample() -> Workbook { let mut wb = Workbook::new(); wb.date_system = DateSystem::D1904; - wb.calc = CalcSettings { mode: CalcMode::Manual, iterative: true, max_iterations: 42, max_change: 0.5, precision_as_displayed: true }; + wb.calc = CalcSettings { mode: CalcMode::Manual, iterative: true, max_iterations: 42, max_change: 0.5, precision_as_displayed: true, r1c1: true }; wb.protected_structure = true; wb.structure_password = Some(gridcraft_model::PasswordHash::new_sha512("s3cret", b"0123456789abcdef")); wb.props = DocProps { @@ -1696,3 +1696,21 @@ fn generated_rule_formulas_use_the_top_left_cell_of_every_area() { z.by_name("xl/worksheets/sheet1.xml").unwrap().read_to_string(&mut xml).unwrap(); assert!(xml.contains("NOT(ISERROR(SEARCH(\"x\",C2)))"), "{xml}"); } + +#[test] +fn r1c1_reference_style_is_kept() { + // #2809: `` was dropped on open, so a workbook saved by Excel in R1C1 + // style came back in A1 style. + let bytes = super::minimal("", &[], "", r#""#); + let (wb, _) = read_xlsx(&bytes).unwrap(); + assert!(wb.calc.r1c1); + let saved = write_xlsx(&wb).unwrap(); + let mut pkg = Package::open(&saved).unwrap(); + let book = pkg.read_xml("xl/workbook.xml").unwrap().unwrap(); + assert_eq!(book.child("calcPr").and_then(|c| c.attr("refMode")), Some("R1C1")); + // A1 is the default and isn't written. + let saved = write_xlsx(&Workbook::new()).unwrap(); + let mut pkg = Package::open(&saved).unwrap(); + let book = pkg.read_xml("xl/workbook.xml").unwrap().unwrap(); + assert_eq!(book.child("calcPr").and_then(|c| c.attr("refMode")), None); +} diff --git a/crates/xlsx/src/write.rs b/crates/xlsx/src/write.rs index 2c1e8f60f..a96e7a350 100644 --- a/crates/xlsx/src/write.rs +++ b/crates/xlsx/src/write.rs @@ -326,6 +326,9 @@ pub fn write_xlsx_named(wb: &Workbook, kind: XlsxKind, book_name: Option<&str>) if wb.calc.precision_as_displayed { w.push_str(" fullPrecision=\"0\""); } + if wb.calc.r1c1 { + w.push_str(" refMode=\"R1C1\""); + } if out.needs_calc { w.push_str(" fullCalcOnLoad=\"1\""); } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index c4bc72bae..f07bb1fcf 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-11-01 · **Last updated:** 2026-11-02 · **Change:** minor (Python cells in `.xlsx` as [MS-XLSX] specifies, `_xlfn._xlws.PY(…)` naming code in the Python part, with cached values, and in GridCraft JSON, with limits on open; earlier: equations in xlsx as Office Math with a marker in `descr` so an edit made in Excel reads back, read back only up to the equation source cap, and a brace with a label and a script on its other side keeps both; Symbol font in PDF; earlier: XLSX reads and writes Page Setup's page order, `pageSetup pageOrder`, and PDF export follows it; earlier: chart data tables: native c:dTable import/export; earlier: XLSX workbook-view selection, normalized tab ratios and sheet-edit regressions; fidelity estimates unchanged; earlier: formulas from Google Sheets' XLSX export open as the original formula instead of `IFERROR(__xludf.DUMMYFUNCTION(…),…)` when GridCraft calculates every function in it, and save as it; `file.exportCsv {values: true}` writes raw values for automation; .xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; HTML export keeps right-to-left; CSV and text import detects likely Windows-1256 Arabic when invalid UTF-8 has Arabic byte runs; other invalid UTF-8 remains Windows-1252; HTML export places pictures and charts, as inline SVG, at their anchors; earlier: `file.exportCsv {values: true}` writes raw values for automation; .xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; HTML export keeps right-to-left; CSV and text import detects likely Windows-1256 Arabic when invalid UTF-8 has Arabic byte runs; other invalid UTF-8 remains Windows-1252; HTML export places pictures and charts, as inline SVG, at their anchors; CSV and text open detects each column's date order from values that read only one way, warns when every date is ambiguous, and keeps a column with both kinds as text (#2765)) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-11-01 · **Last updated:** 2026-11-03 · **Change:** minor (a workbook's R1C1 reference style, `calcPr refMode`, is kept through open and save; earlier: Python cells in `.xlsx` as [MS-XLSX] specifies, `_xlfn._xlws.PY(…)` naming code in the Python part, with cached values, and in GridCraft JSON, with limits on open; earlier: equations in xlsx as Office Math with a marker in `descr` so an edit made in Excel reads back, read back only up to the equation source cap, and a brace with a label and a script on its other side keeps both; Symbol font in PDF; earlier: XLSX reads and writes Page Setup's page order, `pageSetup pageOrder`, and PDF export follows it; earlier: chart data tables: native c:dTable import/export; earlier: XLSX workbook-view selection, normalized tab ratios and sheet-edit regressions; fidelity estimates unchanged; earlier: formulas from Google Sheets' XLSX export open as the original formula instead of `IFERROR(__xludf.DUMMYFUNCTION(…),…)` when GridCraft calculates every function in it, and save as it; `file.exportCsv {values: true}` writes raw values for automation; .xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; HTML export keeps right-to-left; CSV and text import detects likely Windows-1256 Arabic when invalid UTF-8 has Arabic byte runs; other invalid UTF-8 remains Windows-1252; HTML export places pictures and charts, as inline SVG, at their anchors; earlier: `file.exportCsv {values: true}` writes raw values for automation; .xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; HTML export keeps right-to-left; CSV and text import detects likely Windows-1256 Arabic when invalid UTF-8 has Arabic byte runs; other invalid UTF-8 remains Windows-1252; HTML export places pictures and charts, as inline SVG, at their anchors; CSV and text open detects each column's date order from values that read only one way, warns when every date is ambiguous, and keeps a column with both kinds as text (#2765)) · **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** @@ -155,6 +155,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here. | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | XLSX `` (a workbook saved in R1C1 reference style) is read into the calculation settings and written back; GridCraft itself still shows A1 references (#2809, file part) | | 2026-11-02 | minor | Python cells: `.xlsx` reads and writes `_xlfn._xlws.PY(i,0 or 1)` with the code in the Python part (and reads the string-literal form) with cached values and spills, other formats adopt PY formulas, GridCraft JSON keeps the whole cell, and opening limits code size, result size and spill ranges | | 2026-11-02 | trivial | An equation without Office Math takes its stored source only while its text, paragraphs included, is what the writer wrote for it; a paragraph split that changes no character reads back as a text box with the split text | | 2026-11-02 | trivial | Each equation box carries the marker of its own body in its description, the Office Math box and the plain-text fallback alike, and the reader takes the stored source only while the body it reads matches the marker: the text of an equation without Office Math that another application edited reads back as a text box with the edited text instead of the stale source |