diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index 17a0e6649..4a90a6775 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -377,11 +377,23 @@ pub struct Calc { /// Cells whose formulas have errors from cycles. pub circular: Vec, pub last_recalc_cells: usize, + /// The cells whose value is `#SPILL!`, kept so that finding the formulas an emptied cell or a + /// freed spill area may unblock doesn't walk every formula or window of cells. Filled in + /// `rebuild`, kept up to date by `cells_changed` and by the write-back of a run; an entry is + /// always checked against the workbook before it is used. + spill_errors: BTreeSet, } impl Default for Calc { fn default() -> Self { - Calc { graph: Graph::default(), rng: 0x9E37_79B9_7F4A_7C15, fixed_now: None, circular: vec![], last_recalc_cells: 0 } + Calc { + graph: Graph::default(), + rng: 0x9E37_79B9_7F4A_7C15, + fixed_now: None, + circular: vec![], + last_recalc_cells: 0, + spill_errors: BTreeSet::new(), + } } } @@ -1007,8 +1019,12 @@ impl Calc { pub fn rebuild(&mut self, wb: &Workbook) { self.graph = Graph::default(); let mut circ = Vec::new(); + self.spill_errors.clear(); for (si, sheet) in wb.sheets.iter().enumerate() { for (c, cell) in sheet.cells.iter() { + if cell.value == Value::Error(CellError::Spill) { + self.spill_errors.insert((si, c)); + } if let Some(f) = &cell.formula && let Some(e) = f.expr() { @@ -1066,6 +1082,25 @@ impl Calc { out } + /// The cells of `sheet` in `rows` x `cols` (inclusive, `cols` a lower and an upper column) that + /// show `#SPILL!`, in row-major order. + fn spill_errors_in(&self, wb: &Workbook, sheet: usize, rows: (u32, u32), cols: (u32, u32)) -> Vec { + let Some(sh) = wb.sheet(sheet) else { return Vec::new() }; + self.spill_errors + .range((sheet, CellRef::new(rows.0, 0))..=(sheet, CellRef::new(rows.1, u32::MAX))) + .filter(|(_, c)| c.col >= cols.0 && c.col <= cols.1) + .filter(|(_, c)| sh.cells.get(*c).is_some_and(|x| x.value == Value::Error(CellError::Spill))) + .map(|(_, c)| *c) + .collect() + } + + /// The formulas showing `#SPILL!` on `sheet` at or above and to the left of `end`: what a + /// cell emptied or a spill area freed there may unblock. + fn unblock_candidates(&self, wb: &Workbook, sheet: usize, end: CellRef) -> Vec { + let cells = self.spill_errors_in(wb, sheet, (0, end.row), (0, end.col)); + cells.into_iter().filter(|c| self.graph.nodes.contains_key(&(sheet, *c))).map(|c| (sheet, c)).collect() + } + /// Recalculates every formula (F9 / Ctrl+Alt+F9, and after loading). pub fn recalc_all(&mut self, wb: &mut Workbook) { self.rebuild(wb); @@ -1087,6 +1122,11 @@ impl Calc { pub fn cells_changed(&mut self, wb: &mut Workbook, changed: &[Key]) { let mut cleared: Vec<(usize, RangeRef)> = Vec::new(); for &k in changed { + if wb.sheet(k.0).and_then(|s| s.cell(k.1)).is_some_and(|c| c.value == Value::Error(CellError::Spill)) { + self.spill_errors.insert(k); + } else { + self.spill_errors.remove(&k); + } let formula = wb.sheet(k.0).and_then(|s| s.cell(k.1)).and_then(|c| c.formula.clone()); match formula.and_then(|f| f.expr()) { Some(e) => { @@ -1109,6 +1149,7 @@ impl Calc { && cell.formula.is_some() { cell.value = Value::Error(CellError::Name); + self.spill_errors.remove(&k); } } } @@ -1131,10 +1172,9 @@ impl Calc { seeds.push((k.0, *anchor)); } } - for (c, cell) in sh.cells.iter_range(RangeRef::new(CellRef::new(k.1.row.saturating_sub(64), k.1.col.saturating_sub(64)), k.1)) { - if c != k.1 && cell.value == Value::Error(CellError::Spill) { - seeds.push((k.0, c)); - } + if !self.spill_errors.is_empty() { + let (r0, c0) = (k.1.row.saturating_sub(64), k.1.col.saturating_sub(64)); + seeds.extend(self.spill_errors_in(wb, k.0, (r0, k.1.row), (c0, k.1.col)).into_iter().filter(|c| *c != k.1).map(|c| (k.0, c))); } if !sh.cells.has(k.1) { match emptied.iter_mut().find(|x| x.0 == k.0) { @@ -1146,11 +1186,7 @@ impl Calc { } // An emptied cell can unblock an anchor any distance above or to the left of it. for (si, end) in emptied { - for &(s, c) in self.graph.nodes.keys() { - if s == si && c.row <= end.row && c.col <= end.col && wb.sheet(s).is_some_and(|sh| sh.value(c) == Value::Error(CellError::Spill)) { - seeds.push((s, c)); - } - } + seeds.extend(self.unblock_candidates(wb, si, end)); } // Spill areas of changed anchors also change. for &k in changed { @@ -1254,10 +1290,15 @@ impl Calc { let mut freed: Vec<(usize, RangeRef)> = Vec::new(); for (k, v) in results { let Some(sheet) = wb.sheet_mut(k.0) else { continue }; - if let Some(cell) = sheet.cells.get_mut(k.1) - && cell.value != v - { - cell.value = v; + if let Some(cell) = sheet.cells.get_mut(k.1) { + if v == Value::Error(CellError::Spill) { + self.spill_errors.insert(k); + } else { + self.spill_errors.remove(&k); + } + if cell.value != v { + cell.value = v; + } } } for k in &dirty { @@ -1293,6 +1334,7 @@ impl Calc { if blocked { if let Some(cell) = sheet.cells.get_mut(k.1) { cell.value = Value::Error(CellError::Spill); + self.spill_errors.insert(*k); } // Not blocked while the pass ran (it would have been #SPILL! there), so // blocked by another array of this pass: the formulas that read this one, @@ -1338,15 +1380,7 @@ impl Calc { } // An area a formula no longer spills over may unblock another formula's array. for (si, o) in freed { - for &(s, c) in self.graph.nodes.keys() { - if s == si - && c.row <= o.end.row - && c.col <= o.end.col - && wb.sheet(s).is_some_and(|sh| sh.value(c) == Value::Error(CellError::Spill)) - { - spill_changes.push((s, c)); - } - } + spill_changes.extend(self.unblock_candidates(wb, si, o.end)); } if full { // Everything was already evaluated once; spill dependents need another pass. diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs index 5b9e90085..fa8aab3ca 100644 --- a/crates/calc/src/tests.rs +++ b/crates/calc/src/tests.rs @@ -2274,3 +2274,31 @@ fn loan_schedule_residue_keeps_excels_sign() { } assert_eq!(t.get("P7"), Value::Text("".into())); } + +#[test] +fn clearing_a_cell_unblocks_the_arrays_above_and_left_of_it() { + let mut t = T::new(); + for r in 1..=50 { + t.set(&format!("D{r}"), &format!("={r}*2")); + } + t.set("B2", "7"); + t.set("B1", "=SEQUENCE(3)"); + t.set("C1", "=SEQUENCE(1,3)"); + t.set("A20", "=B1+1"); + assert_eq!(t.get("B1"), Value::Error(CellError::Spill)); + assert_eq!(t.get("A20"), Value::Error(CellError::Spill)); + // Emptied: the array above it spills. + t.wb.sheet_mut(0).unwrap().cells.remove(c("B2")); + t.calc.cells_changed(&mut t.wb, &[(0, c("B2"))]); + assert_eq!(t.num("B3"), 3.0); + assert_eq!(t.num("A20"), 2.0); + // Typing over the area blocks it again; far from it, nothing is disturbed. + t.set("B3", "9"); + assert_eq!(t.get("B1"), Value::Error(CellError::Spill)); + t.set("D50", "1"); + assert_eq!(t.get("B1"), Value::Error(CellError::Spill)); + assert_eq!(t.num("D49"), 98.0); + // Clearing it again frees the array. + t.set("B3", ""); + assert_eq!(t.num("B3"), 3.0); +}