1use formualizer_common::{
2 ExcelError, ExcelErrorExtra, ExcelErrorKind, PackedSheetCell, ResourceExhaustionDetail,
3 ResourceExhaustionReason, SheetId,
4};
5
6pub const TARGET_EVAL_DELTA_VERSION: u16 = 1;
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum EvalDeltaCompatibilityPolicy {
14 #[default]
15 Unlimited,
16 CellLimit(usize),
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum DeltaMode {
22 #[default]
24 Off,
25 Cells,
27}
28
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct EvalDelta {
33 pub changed_cells: Vec<PackedSheetCell>,
34}
35
36impl EvalDelta {
37 pub fn is_empty(&self) -> bool {
38 self.changed_cells.is_empty()
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum EvalDeltaRecord {
49 Run {
50 sheet_id: SheetId,
51 start_row: u32,
52 start_col: u32,
53 end_row: u32,
54 end_col: u32,
55 },
56 Region {
57 sheet_id: SheetId,
58 start_row: u32,
59 start_col: u32,
60 end_row: u32,
61 end_col: u32,
62 },
63}
64
65impl EvalDeltaRecord {
66 pub fn sheet_id(&self) -> SheetId {
67 match self {
68 Self::Run { sheet_id, .. } | Self::Region { sheet_id, .. } => *sheet_id,
69 }
70 }
71
72 pub fn bounds(&self) -> (u32, u32, u32, u32) {
73 match self {
74 Self::Run {
75 start_row,
76 start_col,
77 end_row,
78 end_col,
79 ..
80 }
81 | Self::Region {
82 start_row,
83 start_col,
84 end_row,
85 end_col,
86 ..
87 } => (*start_row, *start_col, *end_row, *end_col),
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
97#[non_exhaustive]
98pub struct TargetEvalDelta {
99 pub version: u16,
100 pub records: Vec<EvalDeltaRecord>,
101}
102
103impl Default for TargetEvalDelta {
104 fn default() -> Self {
105 Self {
106 version: TARGET_EVAL_DELTA_VERSION,
107 records: Vec::new(),
108 }
109 }
110}
111
112impl TargetEvalDelta {
113 pub fn is_empty(&self) -> bool {
114 self.records.is_empty()
115 }
116
117 pub fn compatibility_cells(&self, limit: usize) -> Result<EvalDelta, ExcelError> {
118 self.compatibility_cells_with_policy(EvalDeltaCompatibilityPolicy::CellLimit(limit))
119 }
120
121 pub fn compatibility_cells_with_policy(
122 &self,
123 policy: EvalDeltaCompatibilityPolicy,
124 ) -> Result<EvalDelta, ExcelError> {
125 let mut observed = 0usize;
126 for record in &self.records {
127 let (start_row, start_col, end_row, end_col) = record.bounds();
128 let cells = (end_row.saturating_sub(start_row) as usize + 1)
129 .saturating_mul(end_col.saturating_sub(start_col) as usize + 1);
130 observed = observed.saturating_add(cells);
131 if let EvalDeltaCompatibilityPolicy::CellLimit(limit) = policy
132 && observed > limit
133 {
134 return Err(ExcelError::new(ExcelErrorKind::NImpl)
135 .with_message(format!(
136 "target delta compatibility expansion exceeded {limit} cells"
137 ))
138 .with_extra(ExcelErrorExtra::Resource {
139 detail: Box::new(ResourceExhaustionDetail {
140 reason: ResourceExhaustionReason::WorkUnits,
141 limit: limit as u64,
142 observed: observed as u64,
143 request_id: None,
144 }),
145 }));
146 }
147 }
148
149 let mut changed_cells = Vec::new();
150 changed_cells.try_reserve(observed).map_err(|_| {
151 ExcelError::new(ExcelErrorKind::NImpl)
152 .with_message("target delta compatibility expansion allocation failed")
153 .with_extra(ExcelErrorExtra::Resource {
154 detail: Box::new(ResourceExhaustionDetail {
155 reason: ResourceExhaustionReason::ScratchMemory,
156 limit: observed as u64,
157 observed: observed as u64,
158 request_id: None,
159 }),
160 })
161 })?;
162 for record in &self.records {
163 let sheet_id = record.sheet_id();
164 let (start_row, start_col, end_row, end_col) = record.bounds();
165 for row in start_row..=end_row {
166 for col in start_col..=end_col {
167 let packed = PackedSheetCell::try_new(sheet_id, row, col).ok_or_else(|| {
168 ExcelError::new(ExcelErrorKind::NImpl)
169 .with_message("target delta cell exceeds packed compatibility bounds")
170 .with_extra(ExcelErrorExtra::Resource {
171 detail: Box::new(ResourceExhaustionDetail {
172 reason: ResourceExhaustionReason::Admission,
173 limit: u64::from(PackedSheetCell::MAX_ROW0)
174 .saturating_mul(u64::from(PackedSheetCell::MAX_COL0)),
175 observed: u64::from(row).saturating_mul(u64::from(col)),
176 request_id: None,
177 }),
178 })
179 })?;
180 changed_cells.push(packed);
181 }
182 }
183 }
184 changed_cells.sort_unstable();
185 changed_cells.dedup();
186 Ok(EvalDelta { changed_cells })
187 }
188}
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191struct DeltaRect {
192 start_row: u32,
193 start_col: u32,
194 end_row: u32,
195 end_col: u32,
196}
197
198pub(crate) struct DeltaCollector {
199 pub(crate) mode: DeltaMode,
200 changed: Vec<(SheetId, DeltaRect)>,
201}
202
203impl DeltaCollector {
204 pub(crate) fn new(mode: DeltaMode) -> Self {
205 Self {
206 mode,
207 changed: Vec::new(),
208 }
209 }
210
211 #[inline]
212 pub(crate) fn record_cell(&mut self, sheet_id: SheetId, row0: u32, col0: u32) {
213 self.record_region(sheet_id, row0, col0, row0, col0);
214 }
215
216 #[inline]
217 pub(crate) fn record_packed(&mut self, packed: PackedSheetCell) {
218 if self.mode == DeltaMode::Off {
219 return;
220 }
221 self.record_cell(packed.sheet_id(), packed.row0(), packed.col0());
222 }
223
224 pub(crate) fn record_region(
225 &mut self,
226 sheet_id: SheetId,
227 start_row: u32,
228 start_col: u32,
229 end_row: u32,
230 end_col: u32,
231 ) {
232 if self.mode == DeltaMode::Off || start_row > end_row || start_col > end_col {
233 return;
234 }
235 self.changed.push((
236 sheet_id,
237 DeltaRect {
238 start_row,
239 start_col,
240 end_row,
241 end_col,
242 },
243 ));
244 }
245
246 pub(crate) fn finish_target(mut self) -> TargetEvalDelta {
247 if self.mode == DeltaMode::Off {
248 return TargetEvalDelta::default();
249 }
250 self.changed.sort_unstable_by_key(|(sheet_id, record)| {
253 (
254 *sheet_id,
255 record.start_row,
256 record.end_row,
257 record.start_col,
258 record.end_col,
259 )
260 });
261 let mut horizontal: Vec<(SheetId, DeltaRect)> = Vec::with_capacity(self.changed.len());
262 for (sheet_id, record) in self.changed {
263 if let Some((previous_sheet, previous)) = horizontal.last_mut()
264 && *previous_sheet == sheet_id
265 && previous.start_row == record.start_row
266 && previous.end_row == record.end_row
267 && record.start_col <= previous.end_col.saturating_add(1)
268 {
269 previous.end_col = previous.end_col.max(record.end_col);
270 } else {
271 horizontal.push((sheet_id, record));
272 }
273 }
274 horizontal.sort_unstable_by_key(|(sheet_id, record)| {
275 (
276 *sheet_id,
277 record.start_col,
278 record.end_col,
279 record.start_row,
280 record.end_row,
281 )
282 });
283 let mut normalized: Vec<(SheetId, DeltaRect)> = Vec::with_capacity(horizontal.len());
284 for (sheet_id, record) in horizontal {
285 if let Some((previous_sheet, previous)) = normalized.last_mut()
286 && *previous_sheet == sheet_id
287 && previous.start_col == record.start_col
288 && previous.end_col == record.end_col
289 && record.start_row <= previous.end_row.saturating_add(1)
290 {
291 previous.end_row = previous.end_row.max(record.end_row);
292 } else {
293 normalized.push((sheet_id, record));
294 }
295 }
296 normalized.sort_unstable_by_key(|(sheet_id, record)| {
297 (
298 *sheet_id,
299 record.start_row,
300 record.start_col,
301 record.end_row,
302 record.end_col,
303 )
304 });
305 let records = normalized
306 .into_iter()
307 .map(|(sheet_id, record)| {
308 if record.start_row == record.end_row || record.start_col == record.end_col {
309 EvalDeltaRecord::Run {
310 sheet_id,
311 start_row: record.start_row,
312 start_col: record.start_col,
313 end_row: record.end_row,
314 end_col: record.end_col,
315 }
316 } else {
317 EvalDeltaRecord::Region {
318 sheet_id,
319 start_row: record.start_row,
320 start_col: record.start_col,
321 end_row: record.end_row,
322 end_col: record.end_col,
323 }
324 }
325 })
326 .collect();
327 TargetEvalDelta {
328 version: TARGET_EVAL_DELTA_VERSION,
329 records,
330 }
331 }
332
333 pub(crate) fn finish(self) -> Result<EvalDelta, ExcelError> {
334 self.finish_with_policy(EvalDeltaCompatibilityPolicy::Unlimited)
335 }
336
337 pub(crate) fn finish_with_policy(
338 self,
339 policy: EvalDeltaCompatibilityPolicy,
340 ) -> Result<EvalDelta, ExcelError> {
341 self.finish_target().compatibility_cells_with_policy(policy)
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn collector_coalesces_horizontal_and_vertical_runs() {
351 let mut collector = DeltaCollector::new(DeltaMode::Cells);
352 for col in 1..=1000 {
353 collector.record_cell(0, 5, col);
354 }
355 for row in 10..=20 {
356 collector.record_cell(0, row, 8);
357 }
358 let delta = collector.finish_target();
359 assert_eq!(delta.version, TARGET_EVAL_DELTA_VERSION);
360 assert_eq!(delta.records.len(), 2);
361 assert!(
362 delta
363 .records
364 .iter()
365 .all(|record| matches!(record, EvalDeltaRecord::Run { .. }))
366 );
367 }
368
369 #[test]
370 fn legacy_default_expands_more_than_one_hundred_thousand_cells() {
371 let mut collector = DeltaCollector::new(DeltaMode::Cells);
372 collector.record_region(0, 0, 0, 6, 16_383);
373 let delta = collector.finish().unwrap();
374 assert_eq!(delta.changed_cells.len(), 114_688);
375 }
376
377 #[test]
378 fn explicit_compatibility_cap_accepts_cap_and_rejects_cap_plus_one() {
379 let mut at_cap = DeltaCollector::new(DeltaMode::Cells);
380 at_cap.record_region(0, 0, 0, 0, 99);
381 assert_eq!(
382 at_cap
383 .finish_with_policy(EvalDeltaCompatibilityPolicy::CellLimit(100))
384 .unwrap()
385 .changed_cells
386 .len(),
387 100
388 );
389
390 let mut over_cap = DeltaCollector::new(DeltaMode::Cells);
391 over_cap.record_region(0, 0, 0, 0, 100);
392 let error = over_cap
393 .finish_with_policy(EvalDeltaCompatibilityPolicy::CellLimit(100))
394 .unwrap_err();
395 assert!(matches!(error.extra, ExcelErrorExtra::Resource { .. }));
396 }
397
398 #[test]
399 fn compatibility_expansion_returns_typed_overflow_without_truncation() {
400 let delta = TargetEvalDelta {
401 version: TARGET_EVAL_DELTA_VERSION,
402 records: vec![EvalDeltaRecord::Region {
403 sheet_id: 0,
404 start_row: 0,
405 start_col: 0,
406 end_row: 100,
407 end_col: 100,
408 }],
409 };
410 let error = delta.compatibility_cells(100).unwrap_err();
411 assert!(matches!(error.extra, ExcelErrorExtra::Resource { .. }));
412 }
413}