From 2be04524b3a4fcfecfd28a274c30067c3d488aca Mon Sep 17 00:00:00 2001 From: vangel76 Date: Mon, 12 Oct 2026 21:09:27 +0200 Subject: [PATCH] Calc: SUBTOTAL(10x) recalculates when Row Height, AutoFit Row Height or Ungroup reveal rows home.rowHeight, home.autofitRowHeight and data.ungroup change row hidden state without marking the edit structural, so SUBTOTAL(101-111) kept a stale value. Found by the command-sequence fuzz (seed 3). Co-Authored-By: Claude Sonnet 5.5 --- ROADMAP.md | 1 + crates/engine/src/cmd/data.rs | 1 + crates/engine/src/cmd/format.rs | 2 ++ crates/engine/src/tests.rs | 16 ++++++++++++++++ 4 files changed, 20 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index e8e1c06..fafa759 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| +| 2026-10-12 | SUBTOTAL(109) over hidden rows recalculates after Row Height, AutoFit Row Height and Ungroup reveal rows (found by the command-sequence fuzz) | | 2026-10-12 | File › Print selects a system printer, copies, duplex and grayscale, submits PDF jobs through craft-print on a worker and shows spooler errors; browser printing and PDF export remain available | | 2026-10-11 | Command-sequence fuzzing: random command sequences checked against undo/redo, XLSX round-trip and full recalculation; fixes for the table, legacy-array, spill, AutoSum, Watch Window and sheet-copy bugs it found | | 2026-10-11 | Password-protected workbooks open: GridCraft asks for the password and decrypts Agile and Standard encryption (#11) | diff --git a/crates/engine/src/cmd/data.rs b/crates/engine/src/cmd/data.rs index e4aa3ba..d2b3612 100644 --- a/crates/engine/src/cmd/data.rs +++ b/crates/engine/src/cmd/data.rs @@ -1129,6 +1129,7 @@ fn group(s: &mut Session, p: &Json, on: bool) -> Result { { e.collapsed = false; } + cx.structural = true; // ungrouping reveals hidden rows: SUBTOTAL(10x) depends on it Ok(Json::Null) }) } diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index dde5fa1..8d46010 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -953,6 +953,7 @@ fn row_height(s: &mut Session, p: &Json) -> Result { e.custom = true; } } + cx.structural = true; // setting a height shows or hides rows: SUBTOTAL(10x) depends on it Ok(Json::Null) }) } @@ -1069,6 +1070,7 @@ fn autofit_rows(s: &mut Session, p: &Json) -> Result { } cx.fit_rows.extend((a..=b).map(|r| (si, r))); } + cx.structural = true; // autofit reveals hidden rows: SUBTOTAL(10x) depends on it Ok(Json::Null) }) } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 33fdc11..1e78b37 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2795,3 +2795,19 @@ fn a_decrypted_workbook_never_overwrites_its_encrypted_original() { assert_eq!(std::fs::read(&path).unwrap(), bytes, "the encrypted original is untouched"); std::fs::remove_dir_all(dir).unwrap(); } + +#[test] +fn subtotal_109_recalculates_when_row_height_or_autofit_reveals_rows() { + let mut s = s(); + s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [4], [8]]})).unwrap(); + s.execute("cell.set", json!({"cell": "C1", "input": "=SUBTOTAL(109,A1:A4)"})).unwrap(); + s.execute("home.hideRows", json!({"rows": "2:3"})).unwrap(); + assert_eq!(v(&s, "C1"), Value::Number(9.0)); + // A non-zero height unhides the rows (the fuzz found the stale SUBTOTAL). + s.execute("home.rowHeight", json!({"rows": "2:3", "height": 60.0})).unwrap(); + assert_eq!(v(&s, "C1"), Value::Number(15.0)); + s.execute("home.hideRows", json!({"rows": "2:3"})).unwrap(); + assert_eq!(v(&s, "C1"), Value::Number(9.0)); + s.execute("home.autofitRowHeight", json!({"rows": "2:3"})).unwrap(); + assert_eq!(v(&s, "C1"), Value::Number(15.0)); +}