Skip to main content

ironcalc_base/user_model/
autofill.rs

1#![deny(missing_docs)]
2
3use std::collections::HashMap;
4
5use crate::{
6    expressions::{
7        types::Area,
8        utils::{is_valid_column_number, is_valid_row},
9    },
10    model::CellStructure,
11    user_model::sequence_detector::detect_progression,
12    UserModel,
13};
14
15use crate::user_model::history::Diff;
16
17impl<'a> UserModel<'a> {
18    /// Scans the fill target rectangle (`row_start..=row_end`, `col_start..=col_end`) for
19    /// CSE array formulas and prepares them for overwriting:
20    ///
21    /// * A CSE formula **completely** inside the rectangle is cleared; its original cell
22    ///   values are returned keyed by `(row, col)` so the caller can emit correct undo diffs.
23    /// * A CSE formula only **partially** overlapping the rectangle causes an immediate error.
24    fn collect_and_clear_cse_in_fill_target(
25        &mut self,
26        sheet: u32,
27        row_start: i32,
28        row_end: i32,
29        col_start: i32,
30        col_end: i32,
31    ) -> Result<HashMap<(i32, i32), Option<crate::types::Cell>>, String> {
32        // First pass: validate all affected CSE anchors without mutating the worksheet.
33        // An error here leaves the sheet untouched.
34        let mut anchors: Vec<(i32, i32, i32, i32)> = Vec::new();
35        let mut handled: Vec<(i32, i32)> = Vec::new();
36        for row in row_start..=row_end {
37            for col in col_start..=col_end {
38                let (ar, ac, w, h) = match self.model.get_cell_structure(sheet, row, col)? {
39                    CellStructure::ArrayFormula { range: (w, h) } if w > 1 || h > 1 => {
40                        (row, col, w, h)
41                    }
42                    CellStructure::SpillArray {
43                        anchor: (ar, ac),
44                        range: (w, h),
45                    } => (ar, ac, w, h),
46                    _ => continue,
47                };
48                if handled.contains(&(ar, ac)) {
49                    continue;
50                }
51                handled.push((ar, ac));
52                let completely_covered = ar >= row_start
53                    && ar + h - 1 <= row_end
54                    && ac >= col_start
55                    && ac + w - 1 <= col_end;
56                if !completely_covered {
57                    return Err(
58                        "Cannot autofill: selection partially overlaps an array formula"
59                            .to_string(),
60                    );
61                }
62                anchors.push((ar, ac, w, h));
63            }
64        }
65
66        // Second pass: all anchors are completely covered — safe to save and clear.
67        let mut saved: HashMap<(i32, i32), Option<crate::types::Cell>> = HashMap::new();
68        for (ar, ac, w, h) in anchors {
69            for r in ar..ar + h {
70                for c in ac..ac + w {
71                    let cell = self.model.workbook.worksheet(sheet)?.cell(r, c).cloned();
72                    saved.insert((r, c), cell);
73                }
74            }
75            let ws = self.model.workbook.worksheet_mut(sheet)?;
76            for r in ar..ar + h {
77                for c in ac..ac + w {
78                    let _ = ws.cell_clear_contents(r, c);
79                }
80            }
81        }
82        Ok(saved)
83    }
84
85    /// Fills the cells from `source_area` until `to_row`.
86    /// This simulates the user clicking on the cell outline handle and dragging it downwards (or upwards)
87    pub fn auto_fill_rows(&mut self, source_area: &Area, to_row: i32) -> Result<(), String> {
88        let mut diff_list = Vec::new();
89        let sheet = source_area.sheet;
90        let row1 = source_area.row;
91        let column1 = source_area.column;
92        let width = source_area.width;
93        let height = source_area.height;
94
95        // Check first all parameters are valid
96        if self.model.workbook.worksheet(sheet).is_err() {
97            return Err(format!("Invalid worksheet index: '{sheet}'"));
98        }
99
100        if !is_valid_column_number(column1) {
101            return Err(format!("Invalid column: '{column1}'"));
102        }
103        if !is_valid_row(row1) {
104            return Err(format!("Invalid row: '{row1}'"));
105        }
106        if width <= 0 || height <= 0 {
107            return Err(format!("Invalid width='{}' or height='{}'", width, height));
108        }
109
110        let last_column = column1 + width - 1;
111        let last_row = row1 + height - 1;
112
113        if !is_valid_column_number(last_column) {
114            return Err(format!("Invalid column: '{last_column}'"));
115        }
116        if !is_valid_row(last_row) {
117            return Err(format!("Invalid row: '{last_row}'"));
118        }
119
120        if !is_valid_row(to_row) {
121            return Err(format!("Invalid row: '{to_row}'"));
122        }
123
124        // anchor_row is the first row that repeats in each case.
125        let anchor_row;
126        let sign;
127        // this is the range of rows we are going to fill
128        let row_range: Vec<i32>;
129
130        if to_row > last_row {
131            // we go downwards, we start from `row1 + height1` to `to_row`,
132            anchor_row = row1;
133            sign = 1;
134            row_range = (last_row + 1..=to_row).collect();
135        } else if to_row < row1 {
136            // we go upwards, starting from `row1 - 1` all the way to `to_row`
137            anchor_row = last_row;
138            sign = -1;
139            row_range = (to_row..row1).rev().collect();
140        } else {
141            return Err("Invalid parameters for autofill".to_string());
142        }
143
144        // Fill target: rows in row_range, all source columns.
145        let fill_row_start = if sign < 0 { to_row } else { last_row + 1 };
146        let fill_row_end = if sign < 0 { row1 - 1 } else { to_row };
147        let saved_cse = self.collect_and_clear_cse_in_fill_target(
148            sheet,
149            fill_row_start,
150            fill_row_end,
151            column1,
152            last_column,
153        )?;
154
155        for column in column1..=last_column {
156            let mut index = 0;
157            let locale = &self.model.locale;
158            let values = if sign < 0 {
159                (row1..=last_row)
160                    .rev()
161                    .map(|row| self.get_cell_content(sheet, row, column))
162                    .collect::<Result<Vec<_>, _>>()?
163            } else {
164                (row1..=last_row)
165                    .map(|row| self.get_cell_content(sheet, row, column))
166                    .collect::<Result<Vec<_>, _>>()?
167            };
168            let case_seed = self.get_cell_content(sheet, row1, column)?;
169            let possible_progression = detect_progression(&values, locale, &case_seed);
170            for (range_idx, row_ref) in row_range.iter().enumerate() {
171                let row = *row_ref;
172
173                let old_value = saved_cse.get(&(row, column)).cloned().unwrap_or_else(|| {
174                    self.model
175                        .workbook
176                        .worksheet(sheet)
177                        .ok()
178                        .and_then(|ws| ws.cell(row, column).cloned())
179                });
180                let old_style = self.model.get_cell_style_or_none(sheet, row, column)?;
181
182                let source_row = anchor_row + index;
183                let target_value;
184
185                // compute the new value and set it
186                if let Some(ref detected_progression) = possible_progression {
187                    target_value = detected_progression.next(range_idx);
188                } else {
189                    target_value = self
190                        .model
191                        .extend_to(sheet, source_row, column, row, column)?;
192                }
193
194                self.model
195                    .set_user_input(sheet, row, column, target_value.to_string())?;
196
197                // Compute the new style and set it
198                let new_style = self.model.get_style_for_cell(sheet, source_row, column)?;
199                self.model.set_cell_style(sheet, row, column, &new_style)?;
200
201                // Add the diffs
202                diff_list.push(Diff::SetCellStyle {
203                    sheet,
204                    row,
205                    column,
206                    old_value: Box::new(old_style),
207                    new_value: Box::new(new_style),
208                });
209                diff_list.push(Diff::SetCellValue {
210                    sheet,
211                    row,
212                    column,
213                    new_value: target_value.to_string(),
214                    old_value: Box::new(old_value),
215                });
216
217                self.fill_cell_link(sheet, source_row, column, row, column, &mut diff_list)?;
218
219                index = (index + sign) % source_area.height;
220            }
221        }
222        self.push_diff_list(diff_list);
223        self.evaluate();
224        Ok(())
225    }
226
227    /// Fills the cells from `source_area` until `to_column`.
228    /// This simulates the user clicking on the cell outline handle and dragging it to the right (or to the left)
229    pub fn auto_fill_columns(&mut self, source_area: &Area, to_column: i32) -> Result<(), String> {
230        let mut diff_list = Vec::new();
231        let sheet = source_area.sheet;
232        let row1 = source_area.row;
233        let column1 = source_area.column;
234        let width = source_area.width;
235        let height = source_area.height;
236
237        // Check first all parameters are valid
238        if self.model.workbook.worksheet(sheet).is_err() {
239            return Err(format!("Invalid worksheet index: '{sheet}'"));
240        }
241
242        if !is_valid_column_number(column1) {
243            return Err(format!("Invalid column: '{column1}'"));
244        }
245        if !is_valid_row(row1) {
246            return Err(format!("Invalid row: '{row1}'"));
247        }
248        if width <= 0 || height <= 0 {
249            return Err(format!("Invalid width='{}' or height='{}'", width, height));
250        }
251
252        let last_column = column1 + width - 1;
253        let last_row = row1 + height - 1;
254
255        if !is_valid_column_number(last_column) {
256            return Err(format!("Invalid column: '{last_column}'"));
257        }
258        if !is_valid_row(last_row) {
259            return Err(format!("Invalid row: '{last_row}'"));
260        }
261
262        if !is_valid_column_number(to_column) {
263            return Err(format!("Invalid column: '{to_column}'"));
264        }
265
266        // anchor_column is the first column that repeats in each case.
267        let anchor_column;
268        let sign;
269        // this is the range of columns we are going to fill
270        let column_range: Vec<i32>;
271
272        if to_column > last_column {
273            // we go right, we start from `last_column + 1` to `to_column`,
274            anchor_column = column1;
275            sign = 1;
276            column_range = (last_column + 1..to_column + 1).collect();
277        } else if to_column < column1 {
278            // we go left, starting from `column1 - 1` all the way to `to_column`
279            anchor_column = last_column;
280            sign = -1;
281            column_range = (to_column..column1).rev().collect();
282        } else {
283            return Err("Invalid parameters for autofill".to_string());
284        }
285
286        // Fill target: all source rows, columns in column_range.
287        let fill_col_start = if sign < 0 { to_column } else { last_column + 1 };
288        let fill_col_end = if sign < 0 { column1 - 1 } else { to_column };
289        let saved_cse = self.collect_and_clear_cse_in_fill_target(
290            sheet,
291            row1,
292            last_row,
293            fill_col_start,
294            fill_col_end,
295        )?;
296
297        for row in row1..=last_row {
298            let mut index = 0;
299            let locale = &self.model.locale;
300            let values = if sign < 0 {
301                (column1..=last_column)
302                    .rev()
303                    .map(|column| self.get_cell_content(sheet, row, column))
304                    .collect::<Result<Vec<_>, _>>()?
305            } else {
306                (column1..=last_column)
307                    .map(|column| self.get_cell_content(sheet, row, column))
308                    .collect::<Result<Vec<_>, _>>()?
309            };
310            let case_seed = self.get_cell_content(sheet, row, column1)?;
311            let possible_progression = detect_progression(&values, locale, &case_seed);
312            for (range_idx, column_ref) in column_range.iter().enumerate() {
313                let column = *column_ref;
314
315                // Save value and style first
316                let old_value = saved_cse.get(&(row, column)).cloned().unwrap_or_else(|| {
317                    self.model
318                        .workbook
319                        .worksheet(sheet)
320                        .ok()
321                        .and_then(|ws| ws.cell(row, column).cloned())
322                });
323                let old_style = self.model.get_cell_style_or_none(sheet, row, column)?;
324
325                let source_column = anchor_column + index;
326                let target_value;
327
328                // compute the new value and set it
329                if let Some(ref detected_progression) = possible_progression {
330                    target_value = detected_progression.next(range_idx);
331                } else {
332                    target_value = self
333                        .model
334                        .extend_to(sheet, row, source_column, row, column)?;
335                }
336
337                self.model
338                    .set_user_input(sheet, row, column, target_value.to_string())?;
339
340                let new_style = self.model.get_style_for_cell(sheet, row, source_column)?;
341                // Compute the new style and set it
342
343                self.model.set_cell_style(sheet, row, column, &new_style)?;
344
345                // Add the diffs
346                diff_list.push(Diff::SetCellStyle {
347                    sheet,
348                    row,
349                    column,
350                    old_value: Box::new(old_style),
351                    new_value: Box::new(new_style),
352                });
353
354                diff_list.push(Diff::SetCellValue {
355                    sheet,
356                    row,
357                    column,
358                    new_value: target_value.to_string(),
359                    old_value: Box::new(old_value),
360                });
361
362                self.fill_cell_link(sheet, row, source_column, row, column, &mut diff_list)?;
363
364                index = (index + sign) % source_area.width;
365            }
366        }
367        self.push_diff_list(diff_list);
368        self.evaluate();
369        Ok(())
370    }
371
372    /// Makes the link of the fill target cell match the one of its source cell,
373    /// adding the corresponding diff. This runs after the value is set so that
374    /// it also overrides any link auto-created by an URL value.
375    fn fill_cell_link(
376        &mut self,
377        sheet: u32,
378        source_row: i32,
379        source_column: i32,
380        row: i32,
381        column: i32,
382        diff_list: &mut Vec<Diff>,
383    ) -> Result<(), String> {
384        let new_link = self.model.get_cell_link(sheet, source_row, source_column)?;
385        let old_link = self.model.get_cell_link(sheet, row, column)?;
386        if old_link == new_link {
387            return Ok(());
388        }
389        match &new_link {
390            Some(link) => self.model.set_cell_link(sheet, row, column, link.clone())?,
391            None => self.model.delete_cell_link(sheet, row, column)?,
392        }
393        diff_list.push(Diff::SetCellLink {
394            sheet,
395            row,
396            column,
397            old_value: Box::new(old_link),
398            new_value: Box::new(new_link),
399        });
400        Ok(())
401    }
402}