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                index = (index + sign) % source_area.height;
218            }
219        }
220        self.push_diff_list(diff_list);
221        self.evaluate();
222        Ok(())
223    }
224
225    /// Fills the cells from `source_area` until `to_column`.
226    /// This simulates the user clicking on the cell outline handle and dragging it to the right (or to the left)
227    pub fn auto_fill_columns(&mut self, source_area: &Area, to_column: i32) -> Result<(), String> {
228        let mut diff_list = Vec::new();
229        let sheet = source_area.sheet;
230        let row1 = source_area.row;
231        let column1 = source_area.column;
232        let width = source_area.width;
233        let height = source_area.height;
234
235        // Check first all parameters are valid
236        if self.model.workbook.worksheet(sheet).is_err() {
237            return Err(format!("Invalid worksheet index: '{sheet}'"));
238        }
239
240        if !is_valid_column_number(column1) {
241            return Err(format!("Invalid column: '{column1}'"));
242        }
243        if !is_valid_row(row1) {
244            return Err(format!("Invalid row: '{row1}'"));
245        }
246        if width <= 0 || height <= 0 {
247            return Err(format!("Invalid width='{}' or height='{}'", width, height));
248        }
249
250        let last_column = column1 + width - 1;
251        let last_row = row1 + height - 1;
252
253        if !is_valid_column_number(last_column) {
254            return Err(format!("Invalid column: '{last_column}'"));
255        }
256        if !is_valid_row(last_row) {
257            return Err(format!("Invalid row: '{last_row}'"));
258        }
259
260        if !is_valid_column_number(to_column) {
261            return Err(format!("Invalid column: '{to_column}'"));
262        }
263
264        // anchor_column is the first column that repeats in each case.
265        let anchor_column;
266        let sign;
267        // this is the range of columns we are going to fill
268        let column_range: Vec<i32>;
269
270        if to_column > last_column {
271            // we go right, we start from `last_column + 1` to `to_column`,
272            anchor_column = column1;
273            sign = 1;
274            column_range = (last_column + 1..to_column + 1).collect();
275        } else if to_column < column1 {
276            // we go left, starting from `column1 - 1` all the way to `to_column`
277            anchor_column = last_column;
278            sign = -1;
279            column_range = (to_column..column1).rev().collect();
280        } else {
281            return Err("Invalid parameters for autofill".to_string());
282        }
283
284        // Fill target: all source rows, columns in column_range.
285        let fill_col_start = if sign < 0 { to_column } else { last_column + 1 };
286        let fill_col_end = if sign < 0 { column1 - 1 } else { to_column };
287        let saved_cse = self.collect_and_clear_cse_in_fill_target(
288            sheet,
289            row1,
290            last_row,
291            fill_col_start,
292            fill_col_end,
293        )?;
294
295        for row in row1..=last_row {
296            let mut index = 0;
297            let locale = &self.model.locale;
298            let values = if sign < 0 {
299                (column1..=last_column)
300                    .rev()
301                    .map(|column| self.get_cell_content(sheet, row, column))
302                    .collect::<Result<Vec<_>, _>>()?
303            } else {
304                (column1..=last_column)
305                    .map(|column| self.get_cell_content(sheet, row, column))
306                    .collect::<Result<Vec<_>, _>>()?
307            };
308            let case_seed = self.get_cell_content(sheet, row, column1)?;
309            let possible_progression = detect_progression(&values, locale, &case_seed);
310            for (range_idx, column_ref) in column_range.iter().enumerate() {
311                let column = *column_ref;
312
313                // Save value and style first
314                let old_value = saved_cse.get(&(row, column)).cloned().unwrap_or_else(|| {
315                    self.model
316                        .workbook
317                        .worksheet(sheet)
318                        .ok()
319                        .and_then(|ws| ws.cell(row, column).cloned())
320                });
321                let old_style = self.model.get_cell_style_or_none(sheet, row, column)?;
322
323                let source_column = anchor_column + index;
324                let target_value;
325
326                // compute the new value and set it
327                if let Some(ref detected_progression) = possible_progression {
328                    target_value = detected_progression.next(range_idx);
329                } else {
330                    target_value = self
331                        .model
332                        .extend_to(sheet, row, source_column, row, column)?;
333                }
334
335                self.model
336                    .set_user_input(sheet, row, column, target_value.to_string())?;
337
338                let new_style = self.model.get_style_for_cell(sheet, row, source_column)?;
339                // Compute the new style and set it
340
341                self.model.set_cell_style(sheet, row, column, &new_style)?;
342
343                // Add the diffs
344                diff_list.push(Diff::SetCellStyle {
345                    sheet,
346                    row,
347                    column,
348                    old_value: Box::new(old_style),
349                    new_value: Box::new(new_style),
350                });
351
352                diff_list.push(Diff::SetCellValue {
353                    sheet,
354                    row,
355                    column,
356                    new_value: target_value.to_string(),
357                    old_value: Box::new(old_value),
358                });
359
360                index = (index + sign) % source_area.width;
361            }
362        }
363        self.push_diff_list(diff_list);
364        self.evaluate();
365        Ok(())
366    }
367}