diff --git a/crates/xlsx/src/tests/xls_tests.rs b/crates/xlsx/src/tests/xls_tests.rs index 5f8073443..f284188aa 100644 --- a/crates/xlsx/src/tests/xls_tests.rs +++ b/crates/xlsx/src/tests/xls_tests.rs @@ -479,18 +479,21 @@ fn sheets_share_one_record_budget() { #[test] fn reads_iterative_calculation_settings() { - // CalcCount, CalcIter and CalcDelta repeat in every worksheet substream; here only the first - // sheet carries them, so the sheet after it must not reset them to the defaults. - let calc = cat(&[&rec(0x000C, &u16s(&[50])), &rec(0x0011, &u16s(&[1])), &rec(0x0010, &0.01f64.to_le_bytes())]); + // CalcMode, CalcCount, CalcIter and CalcDelta repeat in every worksheet substream; here only + // the first sheet carries them, so the sheet after it must not reset them to the defaults. + // CalcPrecision sits in the globals. + let calc = cat(&[&rec(0x000D, &u16s(&[0])), &rec(0x000C, &u16s(&[50])), &rec(0x0011, &u16s(&[1])), &rec(0x0010, &0.01f64.to_le_bytes())]); let sheet1 = cat(&[&bof(0x0010), &calc, &eof()]); let sheet2 = cat(&[&bof(0x0010), &eof()]); let boundsheet = |pos: u32, name: &str| rec(0x0085, &cat(&[&pos.to_le_bytes(), &[0, 0, name.len() as u8, 0], name.as_bytes()])); - let globals = |p1: u32, p2: u32| cat(&[&bof(0x0005), &boundsheet(p1, "A"), &boundsheet(p2, "B"), &eof()]); + let globals = |p1: u32, p2: u32| cat(&[&bof(0x0005), &rec(0x000E, &u16s(&[0])), &boundsheet(p1, "A"), &boundsheet(p2, "B"), &eof()]); let p1 = globals(0, 0).len() as u32; let p2 = p1 + sheet1.len() as u32; let (wb, _) = read_xls(&compound("Workbook", &cat(&[&globals(p1, p2), &sheet1, &sheet2]))).unwrap(); assert!(wb.calc.iterative); assert_eq!((wb.calc.max_iterations, wb.calc.max_change), (50, 0.01)); + assert_eq!(wb.calc.mode, gridcraft_model::CalcMode::Manual); + assert!(wb.calc.precision_as_displayed); // Without the records: Excel's defaults, iteration off. let (wb, _) = read_xls(&compound("Workbook", &sample(false))).unwrap(); assert_eq!(wb.calc, gridcraft_model::CalcSettings::default()); diff --git a/crates/xlsx/src/xls/mod.rs b/crates/xlsx/src/xls/mod.rs index df27a73c3..407a4a464 100644 --- a/crates/xlsx/src/xls/mod.rs +++ b/crates/xlsx/src/xls/mod.rs @@ -4,9 +4,10 @@ //! //! Read: worksheets, shared strings, cell values, formulas (text and cached results, including //! shared and array formulas), number formats, the 1904 date system, merged cells, column -//! widths, row heights, defined names, print areas, print titles and the iterative calculation -//! settings. Everything else (fonts, fills, borders, charts, pictures, comments, conditional -//! formats, validation, macros) is skipped with a warning. Encrypted workbooks and pre-BIFF8 versions are refused. +//! widths, row heights, defined names, print areas, print titles and the calculation settings +//! (mode, iteration, precision as displayed). Everything else (fonts, fills, borders, charts, +//! pictures, comments, conditional formats, validation, macros) is skipped with a warning. +//! Encrypted workbooks and pre-BIFF8 versions are refused. mod formula; mod ftab; @@ -16,7 +17,7 @@ use std::collections::HashMap; use std::sync::Arc; use gridcraft_core::{CellRef, DateSystem, MAX_COLS, MAX_ROWS, RangeRef, Value}; -use gridcraft_model::{CalcSettings, Cell, DefinedName, Formula, LineInfo, NumFmt, Sheet, Style, StyleId, Visibility, Workbook}; +use gridcraft_model::{CalcMode, CalcSettings, Cell, DefinedName, Formula, LineInfo, NumFmt, Sheet, Style, StyleId, Visibility, Workbook}; use crate::cfb::Compound; use crate::tables::{builtin_format, col_width_to_px, pt_to_px}; @@ -123,6 +124,7 @@ struct Globals { sst: Vec>, links: Links, names: Vec, + precision_as_displayed: bool, } /// Reads an Excel 97-2003 workbook. Inspect the report's warnings for what was skipped. @@ -181,7 +183,7 @@ pub(crate) fn read_xls_with(bytes: &[u8], mut budget: Budget) -> Result<(Workboo continue; }; sheet.visibility = bs.visibility; - // Every worksheet repeats the workbook's iteration settings; Excel applies the first. + // Every worksheet repeats the workbook's calculation settings; Excel applies the first. if wb.sheets.is_empty() { wb.calc = calc; } @@ -191,6 +193,7 @@ pub(crate) fn read_xls_with(bytes: &[u8], mut budget: Budget) -> Result<(Workboo if wb.sheets.is_empty() { return Err(invalid("the workbook has no worksheets GridCraft can read")); } + wb.calc.precision_as_displayed = g.precision_as_displayed; defined_names(&g, &model_index, &mut wb, &mut report); wb.active_sheet = wb.sheets.iter().position(|s| s.visibility == Visibility::Visible).unwrap_or(0); // The overview comes first. @@ -251,6 +254,7 @@ fn globals(stream: &[u8], budget: &mut Budget) -> Result { g.sheets.push(BoundSheet { pos, name, visibility, kind }); } 0x0022 => g.date1904 = c.u16()? == 1, + 0x000E => g.precision_as_displayed = c.u16()? == 0, 0x041E => { let ifmt = c.u16()?; let code = c.string()?; @@ -626,6 +630,13 @@ fn sheet_record(rec: &Record<'_>, st: &mut SheetState<'_>, budget: &mut Budget, st.arrays.insert(at, (range, f)); } } + 0x000D => { + st.calc.mode = match c.u16()? as i16 { + 0 => CalcMode::Manual, + -1 => CalcMode::AutomaticExceptTables, + _ => CalcMode::Automatic, + } + } 0x000C => st.calc.set_max_iterations(u32::from(c.u16()?)), 0x0011 => st.calc.iterative = c.u16()? == 1, 0x0010 => st.calc.set_max_change(c.f64()?), diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index ad458ca1e..0bc481396 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 (`.xls` import reads the iterative calculation settings) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (`.xls` import reads the calculation settings) · **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** @@ -22,7 +22,7 @@ support comes from `crates/engine/src/io.rs` (`FileKind`), `crates/xlsx/src` and | `.xlsm` Macro-Enabled Workbook | R/W | yes | yes, macros kept, not run | The VBA project is kept byte-for-byte and written back only into `.xlsm`/`.xltm` (#6); macros never run. Saved with the macro-enabled workbook content type | `roundtrip.rs` (`vba_project_roundtrip`, `package_kind_sets_workbook_content_type`), engine `io.rs` (`macros_are_saved_only_into_xlsm`) | | `.xltx` / `.xltm` templates | R/W | yes | partial | Saved with the template (and macro-enabled template) content type, chosen by extension; `.xltm` keeps a VBA project, `.xltx` drops it; "new from template" behaviour missing | `roundtrip.rs` (`package_kind_sets_workbook_content_type`), engine `io.rs` | | `.xlsb` Binary Workbook | R/W | **values only** (#101) | no | Names, visibility, numbers, text, booleans, errors, merges, cached formula results; no formulas, formatting, charts, names | `xlsb_tests.rs` | -| `.xls` Excel 97-2003 (BIFF8) | R/W | **yes, partial** (#121) | no (Save As `.xlsx`) | Clean-room from [MS-CFB] and [MS-XLS]. Read: worksheets and visibility, shared strings, numbers, text, booleans, errors, formulas (text for the common tokens, shared and array formulas, 3-D refs, names, newer functions called by name) with cached results, number formats (dates display as dates), 1904 date system, merges, column widths, row heights, defined names, print area and titles, iterative calculation settings (enabled, maximum iterations, maximum change). Not read: fonts, fills, borders, alignment, charts, pictures, comments, CF, validation, hyperlinks, freeze panes, external links (formula kept as its value). Chart and macro sheets skipped with a warning; encrypted (FILEPASS) and BIFF5 (Excel 5.0/95) files refused with a message. Opens as a new unsaved workbook. Unverified on a real-world corpus | `xls_tests.rs` | +| `.xls` Excel 97-2003 (BIFF8) | R/W | **yes, partial** (#121) | no (Save As `.xlsx`) | Clean-room from [MS-CFB] and [MS-XLS]. Read: worksheets and visibility, shared strings, numbers, text, booleans, errors, formulas (text for the common tokens, shared and array formulas, 3-D refs, names, newer functions called by name) with cached results, number formats (dates display as dates), 1904 date system, merges, column widths, row heights, defined names, print area and titles, calculation settings (mode, iteration with its maximum iterations and change, precision as displayed). Not read: fonts, fills, borders, alignment, charts, pictures, comments, CF, validation, hyperlinks, freeze panes, external links (formula kept as its value). Chart and macro sheets skipped with a warning; encrypted (FILEPASS) and BIFF5 (Excel 5.0/95) files refused with a message. Opens as a new unsaved workbook. Unverified on a real-world corpus | `xls_tests.rs` | | `.xlt` Excel 97-2003 template | R/W | yes (read as `.xls`) | no | | — | | `.xlam` / `.xla` add-ins | R/W | no | no | VBA; out of scope | — | | `.xml` XML Spreadsheet 2003 | R/W | no | no | Simple to support | — | @@ -78,7 +78,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here. | Date | Change | Summary | |---|---|---| -| 2026-10-12 | minor | `.xls` import reads the iterative calculation settings (CalcIter, CalcCount, CalcDelta), so circular models calculate instead of showing `#CIRC!` (#724) | +| 2026-10-12 | minor | `.xls` import reads the calculation settings (CalcMode, CalcIter, CalcCount, CalcDelta, CalcPrecision), so circular models calculate instead of showing `#CIRC!` (#724) | | 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 |