Skip to main content

ironcalc_base/
actions.rs

1use crate::cf_types::{CfRule, Cfvo};
2use crate::constants::{LAST_COLUMN, LAST_ROW};
3use crate::cut_paste::cf_sqref_anchor;
4use crate::expressions::parser::stringify::{
5    to_localized_string, to_string_displaced, DisplaceData,
6};
7use crate::expressions::parser::Parser as ExprParser;
8use crate::expressions::types::CellReferenceRC;
9use crate::expressions::utils;
10use crate::language::get_default_language;
11use crate::locale::get_default_locale;
12use crate::model::{CellStructure, Model};
13use crate::types::{ArrayKind, Cell};
14
15/// Returns the new row after displacement, or `None` if the row was deleted.
16fn displace_cf_row(row: i32, data: &DisplaceData, sheet: u32) -> Option<i32> {
17    match data {
18        DisplaceData::Row {
19            sheet: s,
20            row: dr,
21            delta,
22        } if *s == sheet => {
23            if row >= *dr {
24                if *delta < 0 && row < *dr - *delta {
25                    None
26                } else {
27                    Some(row + *delta)
28                }
29            } else {
30                Some(row)
31            }
32        }
33        DisplaceData::RowMove {
34            sheet: s,
35            row: mr,
36            delta,
37        } if *s == sheet => {
38            if row == *mr {
39                Some(row + *delta)
40            } else if *delta > 0 && row > *mr && row <= *mr + *delta {
41                Some(row - 1)
42            } else if *delta < 0 && row < *mr && row >= *mr + *delta {
43                Some(row + 1)
44            } else {
45                Some(row)
46            }
47        }
48        _ => Some(row),
49    }
50}
51
52/// Returns the new column after displacement, or `None` if the column was deleted.
53fn displace_cf_col(col: i32, data: &DisplaceData, sheet: u32) -> Option<i32> {
54    match data {
55        DisplaceData::Column {
56            sheet: s,
57            column: dc,
58            delta,
59        } if *s == sheet => {
60            if col >= *dc {
61                if *delta < 0 && col < *dc - *delta {
62                    None
63                } else {
64                    Some(col + *delta)
65                }
66            } else {
67                Some(col)
68            }
69        }
70        DisplaceData::ColumnMove {
71            sheet: s,
72            column: mc,
73            delta,
74        } if *s == sheet => {
75            if col == *mc {
76                Some(col + *delta)
77            } else if *delta > 0 && col > *mc && col <= *mc + *delta {
78                Some(col - 1)
79            } else if *delta < 0 && col < *mc && col >= *mc + *delta {
80                Some(col + 1)
81            } else {
82                Some(col)
83            }
84        }
85        _ => Some(col),
86    }
87}
88
89/// Displaces a single A1-style sqref part (e.g. "A1" or "A1:B5").
90/// Returns the original string unchanged if any corner would become #REF!.
91fn displace_cf_sqref_part(part: &str, data: &DisplaceData, sheet: u32) -> String {
92    let upper = part.to_uppercase();
93    let segs: Vec<&str> = upper.splitn(2, ':').collect();
94    match segs.len() {
95        1 => {
96            if let Some(r) = utils::parse_reference_a1(segs[0]) {
97                if let (Some(nr), Some(nc)) = (
98                    displace_cf_row(r.row, data, sheet),
99                    displace_cf_col(r.column, data, sheet),
100                ) {
101                    if let Some(c) = utils::number_to_column(nc) {
102                        return format!("{c}{nr}");
103                    }
104                }
105            }
106            part.to_string()
107        }
108        2 => {
109            if let (Some(r1), Some(r2)) = (
110                utils::parse_reference_a1(segs[0]),
111                utils::parse_reference_a1(segs[1]),
112            ) {
113                if let (Some(nr1), Some(nc1), Some(nr2), Some(nc2)) = (
114                    displace_cf_row(r1.row, data, sheet),
115                    displace_cf_col(r1.column, data, sheet),
116                    displace_cf_row(r2.row, data, sheet),
117                    displace_cf_col(r2.column, data, sheet),
118                ) {
119                    if let (Some(c1), Some(c2)) =
120                        (utils::number_to_column(nc1), utils::number_to_column(nc2))
121                    {
122                        return format!("{c1}{nr1}:{c2}{nr2}");
123                    }
124                }
125            }
126            part.to_string()
127        }
128        _ => part.to_string(),
129    }
130}
131
132/// Displaces every part of a space-separated sqref string.
133fn displace_cf_sqref(sqref: &str, data: &DisplaceData, sheet: u32) -> String {
134    sqref
135        .split_whitespace()
136        .map(|p| displace_cf_sqref_part(p, data, sheet))
137        .collect::<Vec<_>>()
138        .join(" ")
139}
140
141// NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns
142// In Excel if the whole range is deleted then it will substitute for #REF!
143// In IronCalc, if one of the edges of the range is deleted will replace the edge with #REF!
144// I feel this is unimportant for now.
145
146/// Displaces a single formula string (with or without leading `=`) using `to_string_displaced`.
147/// CF formulas are stored in English (see [Model::user_formula_to_internal]),
148/// so the caller must have the parser in the default (English) locale/language.
149fn displace_cf_formula_str(
150    parser: &mut ExprParser<'_>,
151    formula: &str,
152    context: &CellReferenceRC,
153    data: &DisplaceData,
154) -> String {
155    let trimmed = formula.trim();
156    let has_eq = trimmed.starts_with('=');
157    let body = if has_eq { &trimmed[1..] } else { trimmed };
158    let node = parser.parse(body, context);
159    let displaced = to_string_displaced(
160        &node,
161        context,
162        data,
163        get_default_locale(),
164        get_default_language(),
165    );
166    if has_eq {
167        format!("={displaced}")
168    } else {
169        displaced
170    }
171}
172
173fn displace_cfvo(
174    parser: &mut ExprParser<'_>,
175    cfvo: Cfvo,
176    context: &CellReferenceRC,
177    data: &DisplaceData,
178) -> Cfvo {
179    if let Cfvo::Formula(f) = cfvo {
180        Cfvo::Formula(displace_cf_formula_str(parser, &f, context, data))
181    } else {
182        cfvo
183    }
184}
185
186/// Displaces all formula fields inside a `CfRule`.
187fn displace_cf_rule_formulas(
188    parser: &mut ExprParser<'_>,
189    rule: CfRule,
190    context: &CellReferenceRC,
191    data: &DisplaceData,
192) -> CfRule {
193    match rule {
194        CfRule::Formula {
195            formula,
196            dxf_id,
197            stop_if_true,
198        } => CfRule::Formula {
199            formula: displace_cf_formula_str(parser, &formula, context, data),
200            dxf_id,
201            stop_if_true,
202        },
203        CfRule::CellIs {
204            operator,
205            formula,
206            formula2,
207            dxf_id,
208            stop_if_true,
209        } => CfRule::CellIs {
210            operator,
211            formula: displace_cf_formula_str(parser, &formula, context, data),
212            formula2: formula2.map(|f| displace_cf_formula_str(parser, &f, context, data)),
213            dxf_id,
214            stop_if_true,
215        },
216        CfRule::ColorScale { thresholds } => CfRule::ColorScale {
217            thresholds: thresholds
218                .into_iter()
219                .map(|mut t| {
220                    t.cfvo = displace_cfvo(parser, t.cfvo, context, data);
221                    t
222                })
223                .collect(),
224        },
225        CfRule::DataBar {
226            min,
227            max,
228            positive_color,
229            negative_color,
230            is_gradient,
231            show_value,
232        } => CfRule::DataBar {
233            min: min.map(|c| displace_cfvo(parser, c, context, data)),
234            max: max.map(|c| displace_cfvo(parser, c, context, data)),
235            positive_color,
236            negative_color,
237            is_gradient,
238            show_value,
239        },
240        CfRule::IconSet {
241            thresholds,
242            show_value,
243        } => CfRule::IconSet {
244            thresholds: thresholds
245                .into_iter()
246                .map(|mut t| {
247                    t.cfvo = displace_cfvo(parser, t.cfvo, context, data);
248                    t
249                })
250                .collect(),
251            show_value,
252        },
253        CfRule::IconRating {
254            icon,
255            color,
256            thresholds,
257            show_value,
258        } => CfRule::IconRating {
259            icon,
260            color,
261            thresholds: thresholds
262                .into_iter()
263                .map(|(cfvo, strict)| (displace_cfvo(parser, cfvo, context, data), strict))
264                .collect(),
265            show_value,
266        },
267        // No formula fields in remaining variants
268        other => other,
269    }
270}
271
272impl<'a> Model<'a> {
273    fn shift_cell_formula(
274        &mut self,
275        sheet: u32,
276        row: i32,
277        column: i32,
278        displace_data: &DisplaceData,
279    ) -> Result<(), String> {
280        if let Some(f) = self
281            .workbook
282            .worksheet(sheet)?
283            .cell(row, column)
284            .and_then(|c| c.get_formula())
285        {
286            let node = &self.parsed_formulas[sheet as usize][f as usize].0.clone();
287            let cell_reference = CellReferenceRC {
288                sheet: self.workbook.worksheets[sheet as usize].get_name(),
289                row,
290                column,
291            };
292            // FIXME: This is not a very performant way if the formula has changed :S.
293            // Both strings must be in the active locale/language: the displaced
294            // one is written back through the (localized) parser, and comparing
295            // against an English rendering would flag every formula as changed.
296            let formula = to_localized_string(node, &cell_reference, self.locale, self.language);
297            let formula_displaced = to_string_displaced(
298                node,
299                &cell_reference,
300                displace_data,
301                self.locale,
302                self.language,
303            );
304            if formula != formula_displaced {
305                self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))?;
306            };
307        }
308        Ok(())
309    }
310    /// This function iterates over all cells in the model and shifts their formulas according to the displacement data.
311    ///
312    /// # Arguments
313    ///
314    /// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude.
315    fn displace_cells(&mut self, displace_data: &DisplaceData) -> Result<(), String> {
316        let cells = self.get_all_cells();
317        for cell in cells {
318            self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data)?;
319        }
320        Ok(())
321    }
322
323    /// Updates the `range` field and formula fields of every CF rule on `sheet` according to `displace_data`.
324    fn displace_cf_ranges(&mut self, sheet: u32, displace_data: &DisplaceData) {
325        let count = match self.workbook.worksheets.get(sheet as usize) {
326            Some(ws) => ws.conditional_formatting.len(),
327            None => return,
328        };
329
330        // Phase 1: collect (index, new_range, old_rule, anchor) without holding a borrow on self.
331        let sheet_name = self.workbook.worksheets[sheet as usize].get_name();
332        let mut phase1: Vec<(usize, String, CfRule, i32, i32)> = Vec::with_capacity(count);
333        for idx in 0..count {
334            let cf = &self.workbook.worksheets[sheet as usize].conditional_formatting[idx];
335            let old_range = cf.range.clone();
336            let new_range = displace_cf_sqref(&old_range, displace_data, sheet);
337            let rule = cf.cf_rule.clone();
338            if let Some((anchor_row, anchor_col)) = cf_sqref_anchor(&old_range) {
339                phase1.push((idx, new_range, rule, anchor_row, anchor_col));
340            }
341        }
342
343        // Phase 2: displace formula fields (requires &mut self.parser) then write back.
344        // CF formulas are stored in English, so parse them with the default
345        // locale/language regardless of the active ones.
346        let locale = self.locale;
347        let language = self.language;
348        self.parser.set_locale(get_default_locale());
349        self.parser.set_language(get_default_language());
350        for (idx, new_range, rule, anchor_row, anchor_col) in phase1 {
351            let context = CellReferenceRC {
352                sheet: sheet_name.clone(),
353                row: anchor_row,
354                column: anchor_col,
355            };
356            let new_rule =
357                displace_cf_rule_formulas(&mut self.parser, rule, &context, displace_data);
358            self.workbook.worksheets[sheet as usize].conditional_formatting[idx].range = new_range;
359            self.workbook.worksheets[sheet as usize].conditional_formatting[idx].cf_rule = new_rule;
360        }
361        self.parser.set_locale(locale);
362        self.parser.set_language(language);
363    }
364
365    /// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order.
366    ///
367    /// # Arguments
368    ///
369    /// * `sheet` - The sheet number to retrieve columns from.
370    /// * `row` - The row number to retrieve columns for.
371    /// * `descending` - If true, the columns are returned in descending order; otherwise, in ascending order.
372    ///
373    /// # Returns
374    ///
375    /// This function returns a `Result` containing either:
376    /// - `Ok(Vec<i32>)`: A vector of column indices for the specified row, sorted according to the `descending` flag.
377    /// - `Err(String)`: An error message if the sheet cannot be found.
378    fn get_columns_for_row(
379        &self,
380        sheet: u32,
381        row: i32,
382        descending: bool,
383    ) -> Result<Vec<i32>, String> {
384        let worksheet = self.workbook.worksheet(sheet)?;
385        if let Some(row_data) = worksheet.sheet_data.get(&row) {
386            let mut columns: Vec<i32> = row_data.keys().copied().collect();
387            columns.sort_unstable();
388            if descending {
389                columns.reverse();
390            }
391            Ok(columns)
392        } else {
393            Ok(vec![])
394        }
395    }
396
397    /// Moves the contents of cell (source_row, source_column) to (target_row, target_column).
398    ///
399    /// It assumes that the caller has already checked that the move is valid
400    /// (e.g. it does not split an array formula). And that dynamic array spills have been reset.
401    ///
402    /// # Arguments
403    ///
404    /// * `sheet` - The sheet number to retrieve columns from.
405    /// * `source_row` - The row index of the cell's current location.
406    /// * `source_column` - The column index of the cell's current location.
407    /// * `target_row` - The row index of the cell's new location.
408    /// * `target_column` - The column index of the cell's new location.
409    fn move_cell(
410        &mut self,
411        sheet: u32,
412        source_row: i32,
413        source_column: i32,
414        target_row: i32,
415        target_column: i32,
416    ) -> Result<(), String> {
417        let source_cell = match self
418            .workbook
419            .worksheet(sheet)?
420            .cell(source_row, source_column)
421        {
422            Some(c) => c,
423            None => return Ok(()),
424        };
425        let style = source_cell.get_style();
426
427        let mut array = None;
428
429        match source_cell {
430            Cell::EmptyCell { .. }
431            | Cell::BooleanCell { .. }
432            | Cell::NumberCell { .. }
433            | Cell::ErrorCell { .. }
434            | Cell::SharedString { .. }
435            | Cell::CellFormula { .. } => {
436                // This is a regular cell, we can just move it.
437            }
438            Cell::SpillCell { .. } => {
439                // This the spill of an array formula. Because dynamic arrays spills have been deleted
440                // We delete the spill
441                let worksheet = self.workbook.worksheet_mut(sheet)?;
442                worksheet.remove_cell(source_row, source_column)?;
443                return Ok(());
444            }
445            Cell::ArrayFormula {
446                r,
447                kind: ArrayKind::Dynamic,
448                ..
449            } => {
450                // We are moving the anchor of a dynamic formula.
451                // We assume the spill has been taken care of by the caller
452                debug_assert_eq!(*r, (1, 1));
453            }
454            Cell::ArrayFormula {
455                r,
456                kind: ArrayKind::Cse,
457                ..
458            } => {
459                // This is an array formula, we need to move the whole range
460                // We rely on the calling function to check that the move is valid and does not split the array formula
461                array = Some(*r);
462            }
463        }
464        let formula_or_value = self
465            .get_cell_formula(sheet, source_row, source_column)?
466            .unwrap_or_else(|| {
467                source_cell.get_localized_text(
468                    &self.workbook.shared_strings,
469                    self.locale,
470                    self.language,
471                )
472            });
473
474        if let Some((width, height)) = array {
475            // We are moving an array formula, we need to move the whole range
476            self.set_user_array_formula(
477                sheet,
478                target_row,
479                target_column,
480                width,
481                height,
482                &formula_or_value,
483            )?;
484        } else {
485            self.set_user_input(sheet, target_row, target_column, formula_or_value)?;
486        }
487
488        let worksheet = self.workbook.worksheet_mut(sheet)?;
489        // copy style
490        worksheet.set_cell_style(target_row, target_column, style)?;
491
492        // delete source cell content and style
493        worksheet.remove_cell(source_row, source_column)?;
494        Ok(())
495    }
496
497    /// Inserts one or more new columns into the model at the specified index.
498    ///
499    /// This method shifts existing columns to the right to make space for the new columns.
500    ///
501    /// # Arguments
502    ///
503    /// * `sheet` - The sheet number to retrieve columns from.
504    /// * `column` - The index at which the new columns should be inserted.
505    /// * `column_count` - The number of columns to insert.
506    pub fn insert_columns(
507        &mut self,
508        sheet: u32,
509        column: i32,
510        column_count: i32,
511    ) -> Result<(), String> {
512        if column_count <= 0 {
513            return Err("Cannot add a negative number of cells :)".to_string());
514        }
515        if !self.can_insert_columns(sheet, column, column_count)? {
516            return Err(
517                "Cannot insert columns because that would break an array formula".to_string(),
518            );
519        }
520        // check if it is possible:
521        let dimensions = self.workbook.worksheet(sheet)?.dimension();
522        let last_column = dimensions.max_column + column_count;
523        if last_column > LAST_COLUMN {
524            return Err(
525                "Cannot shift cells because that would delete cells at the end of a row"
526                    .to_string(),
527            );
528        }
529        self.reset_dynamic_array_spills(sheet)?;
530        let worksheet = self.workbook.worksheet(sheet)?;
531        let all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
532        for row in all_rows {
533            let sorted_columns = self.get_columns_for_row(sheet, row, true)?;
534            for col in sorted_columns {
535                if col >= column {
536                    self.move_cell(sheet, row, col, row, col + column_count)?;
537                } else {
538                    // Break because columns are in descending order.
539                    break;
540                }
541            }
542        }
543
544        // Update all formulas in the workbook
545        let disp = DisplaceData::Column {
546            sheet,
547            column,
548            delta: column_count,
549        };
550        self.displace_cells(&disp)?;
551        self.displace_cf_ranges(sheet, &disp);
552
553        // In the list of columns:
554        // * Keep all the columns to the left
555        // * Displace all the columns to the right
556
557        let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
558
559        let mut new_columns = Vec::new();
560        for col in worksheet.cols.iter_mut() {
561            // range under study
562            let min = col.min;
563            let max = col.max;
564            if column > max {
565                // If the range under study is to our left, this is a noop
566            } else if column <= min {
567                // If the range under study is to our right, we displace it
568                col.min = min + column_count;
569                col.max = max + column_count;
570            } else {
571                // If the range under study is in the middle we augment it
572                col.max = max + column_count;
573            }
574            new_columns.push(col.clone());
575        }
576        // TODO: If in a row the cell to the right and left have the same style we should copy it
577
578        worksheet.cols = new_columns;
579
580        Ok(())
581    }
582
583    /// Deletes one or more columns from the model starting at the specified index.
584    ///
585    /// # Arguments
586    ///
587    /// * `sheet` - The sheet number to retrieve columns from.
588    /// * `column` - The index of the first column to delete.
589    /// * `count` - The number of columns to delete.
590    pub fn delete_columns(
591        &mut self,
592        sheet: u32,
593        column: i32,
594        column_count: i32,
595    ) -> Result<(), String> {
596        if column_count <= 0 {
597            return Err("Please use insert columns instead".to_string());
598        }
599        if !(1..=LAST_COLUMN).contains(&column) {
600            return Err(format!("Column number '{column}' is not valid."));
601        }
602        if column + column_count - 1 > LAST_COLUMN {
603            return Err("Cannot delete columns beyond the last column of the sheet".to_string());
604        }
605        if !self.can_delete_columns(sheet, column, column_count)? {
606            return Err(
607                "Cannot delete columns because that would break an array formula".to_string(),
608            );
609        }
610
611        self.reset_dynamic_array_spills(sheet)?;
612        // first column being deleted
613        let column_start = column;
614        // last column being deleted
615        let column_end = column + column_count - 1;
616
617        // Move cells
618        let worksheet = &self.workbook.worksheet(sheet)?;
619        let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
620        // We do not need to do that, but it is safer to eliminate sources of randomness in the algorithm
621        all_rows.sort_unstable();
622
623        for r in all_rows {
624            let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
625            for col in columns {
626                if col >= column_start {
627                    if col > column_end {
628                        self.move_cell(sheet, r, col, r, col - column_count)?;
629                    } else {
630                        self.workbook.worksheet_mut(sheet)?.remove_cell(r, col)?;
631                    }
632                }
633            }
634        }
635        // Update all formulas in the workbook
636        let disp = DisplaceData::Column {
637            sheet,
638            column,
639            delta: -column_count,
640        };
641        self.displace_cells(&disp)?;
642        self.displace_cf_ranges(sheet, &disp);
643        let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
644
645        // deletes all the column styles
646        let mut new_columns = Vec::new();
647        for col in worksheet.cols.iter_mut() {
648            // range under study
649            let min = col.min;
650            let max = col.max;
651            // In the diagram:
652            // |xxxxx| range we are studying [min, max]
653            // |*****| range we are deleting [column_start, column_end]
654            // we are going to split it in three big cases:
655            // ----------------|xxxxxxxx|-----------------
656            // -----|*****|------------------------------- Case A
657            // -------|**********|------------------------ Case B
658            // -------------|**************|-------------- Case C
659            // ------------------|****|------------------- Case D
660            // ---------------------|**********|---------- Case E
661            // -----------------------------|*****|------- Case F
662            if column_start < min {
663                if column_end < min {
664                    // Case A
665                    // We displace all columns
666                    let mut new_column = col.clone();
667                    new_column.min = min - column_count;
668                    new_column.max = max - column_count;
669                    new_columns.push(new_column);
670                } else if column_end < max {
671                    // Case B
672                    // We displace the end
673                    let mut new_column = col.clone();
674                    new_column.min = column_start;
675                    new_column.max = max - column_count;
676                    new_columns.push(new_column);
677                } else {
678                    // Case C
679                    // skip this, we are deleting the whole range
680                }
681            } else if column_start <= max {
682                if column_end <= max {
683                    // Case D
684                    // We displace the end
685                    let mut new_column = col.clone();
686                    new_column.max = max - column_count;
687                    new_columns.push(new_column);
688                } else {
689                    // Case E
690                    let mut new_column = col.clone();
691                    new_column.max = column_start - 1;
692                    new_columns.push(new_column);
693                }
694            } else {
695                // Case F
696                // No action required
697                new_columns.push(col.clone());
698            }
699        }
700        worksheet.cols = new_columns;
701
702        Ok(())
703    }
704
705    // Returns true if inserting rows at `row` would not split any array formula.
706    // Inserting at `row` shifts every row >= `row` down. A formula whose anchor
707    // row is strictly above `row` but whose spill extends to `row` or below would
708    // be split, so we must reject that.
709    fn can_insert_rows(&self, sheet: u32, row: i32, _row_count: i32) -> Result<bool, String> {
710        let cell_coords: Vec<(i32, i32)> = {
711            let worksheet = self.workbook.worksheet(sheet)?;
712            worksheet
713                .sheet_data
714                .iter()
715                .flat_map(|(r, row_data)| row_data.keys().map(move |c| (*r, *c)))
716                .collect()
717        };
718        for (r, c) in cell_coords {
719            if let CellStructure::ArrayFormula { range: (_, height) } =
720                self.get_cell_structure(sheet, r, c)?
721            {
722                // The formula spans rows [r, r + height - 1].
723                // Inserting at `row` splits it when the anchor is above `row`
724                // but the spill reaches `row` or beyond.
725                if r < row && row < r + height {
726                    return Ok(false);
727                }
728            }
729        }
730        Ok(true)
731    }
732
733    // Returns true if inserting columns at `column` would not split any array formula.
734    fn can_insert_columns(
735        &self,
736        sheet: u32,
737        column: i32,
738        _column_count: i32,
739    ) -> Result<bool, String> {
740        let cell_coords: Vec<(i32, i32)> = {
741            let worksheet = self.workbook.worksheet(sheet)?;
742            worksheet
743                .sheet_data
744                .iter()
745                .flat_map(|(r, row_data)| row_data.keys().map(move |c| (*r, *c)))
746                .collect()
747        };
748        for (r, c) in cell_coords {
749            if let CellStructure::ArrayFormula { range: (width, _) } =
750                self.get_cell_structure(sheet, r, c)?
751            {
752                if c < column && column < c + width {
753                    return Ok(false);
754                }
755            }
756        }
757        Ok(true)
758    }
759
760    // Returns true if deleting rows [row, row + row_count - 1] would not break any
761    // array formula. An array formula must be either fully inside the deleted range
762    // or fully outside it; any partial overlap is rejected.
763    fn can_delete_rows(&self, sheet: u32, row: i32, row_count: i32) -> Result<bool, String> {
764        let row_end = row + row_count; // exclusive upper bound
765        let cell_coords: Vec<(i32, i32)> = {
766            let worksheet = self.workbook.worksheet(sheet)?;
767            worksheet
768                .sheet_data
769                .iter()
770                .flat_map(|(r, row_data)| row_data.keys().map(move |c| (*r, *c)))
771                .collect()
772        };
773        for (r, c) in cell_coords {
774            if let CellStructure::ArrayFormula { range: (_, height) } =
775                self.get_cell_structure(sheet, r, c)?
776            {
777                // Formula row span: [r, r + height - 1]
778                let overlaps = r < row_end && r + height > row;
779                let contained = r >= row && r + height <= row_end;
780                if overlaps && !contained {
781                    return Ok(false);
782                }
783            }
784        }
785        Ok(true)
786    }
787
788    // Returns true if deleting columns [column, column + column_count - 1] would not
789    // break any array formula.
790    fn can_delete_columns(
791        &self,
792        sheet: u32,
793        column: i32,
794        column_count: i32,
795    ) -> Result<bool, String> {
796        let col_end = column + column_count; // exclusive upper bound
797        let cell_coords: Vec<(i32, i32)> = {
798            let worksheet = self.workbook.worksheet(sheet)?;
799            worksheet
800                .sheet_data
801                .iter()
802                .flat_map(|(r, row_data)| row_data.keys().map(move |c| (*r, *c)))
803                .collect()
804        };
805        for (r, c) in cell_coords {
806            if let CellStructure::ArrayFormula { range: (width, _) } =
807                self.get_cell_structure(sheet, r, c)?
808            {
809                // Formula column span: [c, c + width - 1]
810                let overlaps = c < col_end && c + width > column;
811                let contained = c >= column && c + width <= col_end;
812                if overlaps && !contained {
813                    return Ok(false);
814                }
815            }
816        }
817        Ok(true)
818    }
819
820    /// Inserts one or more new rows into the model at the specified index.
821    ///
822    /// # Arguments
823    ///
824    /// * `sheet` - The sheet number to retrieve columns from.
825    /// * `row` - The index at which the new rows should be inserted.
826    /// * `row_count` - The number of rows to insert.
827    pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
828        if row_count <= 0 {
829            return Err("Cannot add a negative number of cells :)".to_string());
830        }
831        if !self.can_insert_rows(sheet, row, row_count)? {
832            return Err("Cannot insert rows because that would break an array formula".to_string());
833        }
834        // Check if it is possible:
835        let dimensions = self.workbook.worksheet(sheet)?.dimension();
836        let last_row = dimensions.max_row + row_count;
837        if last_row > LAST_ROW {
838            return Err(
839                "Cannot shift cells because that would delete cells at the end of a column"
840                    .to_string(),
841            );
842        }
843
844        self.reset_dynamic_array_spills(sheet)?;
845        // Move cells
846        let worksheet = &self.workbook.worksheet(sheet)?;
847        let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
848        all_rows.sort_unstable();
849        all_rows.reverse();
850        for r in all_rows {
851            if r >= row {
852                // We do not really need the columns in any order
853                let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
854                for column in columns {
855                    self.move_cell(sheet, r, column, r + row_count, column)?;
856                }
857            } else {
858                // Rows are in descending order
859                break;
860            }
861        }
862        // In the list of rows styles:
863        // * Add all rows above the rows we are inserting unchanged
864        // * Shift the ones below
865        let rows = &self.workbook.worksheets[sheet as usize].rows;
866        let mut new_rows = vec![];
867        for r in rows {
868            if r.r < row {
869                new_rows.push(r.clone());
870            } else if r.r >= row {
871                let mut new_row = r.clone();
872                new_row.r = r.r + row_count;
873                new_rows.push(new_row);
874            }
875        }
876        self.workbook.worksheets[sheet as usize].rows = new_rows;
877
878        // Update all formulas in the workbook
879        let disp = DisplaceData::Row {
880            sheet,
881            row,
882            delta: row_count,
883        };
884        self.displace_cells(&disp)?;
885        self.displace_cf_ranges(sheet, &disp);
886
887        Ok(())
888    }
889
890    /// Deletes one or more rows from the model starting at the specified index.
891    ///
892    /// # Arguments
893    ///
894    /// * `sheet` - The sheet number to retrieve columns from.
895    /// * `row` - The index of the first row to delete.
896    /// * `row_count` - The number of rows to delete.
897    pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
898        if row_count <= 0 {
899            return Err("Please use insert rows instead".to_string());
900        }
901        if !(1..=LAST_ROW).contains(&row) {
902            return Err(format!("Row number '{row}' is not valid."));
903        }
904        if row + row_count - 1 > LAST_ROW {
905            return Err("Cannot delete rows beyond the last row of the sheet".to_string());
906        }
907        if !self.can_delete_rows(sheet, row, row_count)? {
908            return Err("Cannot delete rows because that would break an array formula".to_string());
909        }
910
911        self.reset_dynamic_array_spills(sheet)?;
912        // Move cells
913        let worksheet = &self.workbook.worksheet(sheet)?;
914        let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
915        all_rows.sort_unstable();
916
917        for r in all_rows {
918            if r >= row {
919                // We do not need ordered, but it is safer to eliminate sources of randomness in the algorithm
920                let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
921                if r >= row + row_count {
922                    // displace all cells in column
923                    for column in columns {
924                        self.move_cell(sheet, r, column, r - row_count, column)?;
925                    }
926                } else {
927                    // remove all cells in row
928                    self.workbook.worksheet_mut(sheet)?.sheet_data.remove(&r);
929                }
930            }
931        }
932        // In the list of rows styles:
933        // * Add all rows above the rows we are deleting unchanged
934        // * Skip all those we are deleting
935        // * Shift the ones below
936        let rows = &self.workbook.worksheets[sheet as usize].rows;
937        let mut new_rows = vec![];
938        for r in rows {
939            if r.r < row {
940                new_rows.push(r.clone());
941            } else if r.r >= row + row_count {
942                let mut new_row = r.clone();
943                new_row.r = r.r - row_count;
944                new_rows.push(new_row);
945            }
946        }
947        self.workbook.worksheets[sheet as usize].rows = new_rows;
948        let disp = DisplaceData::Row {
949            sheet,
950            row,
951            delta: -row_count,
952        };
953        self.displace_cells(&disp)?;
954        self.displace_cf_ranges(sheet, &disp);
955        Ok(())
956    }
957
958    // Inner column move: no boundary/can check, no spill reset.
959    // Caller must have validated and reset spills before calling this.
960    fn move_column_unchecked(&mut self, sheet: u32, column: i32, delta: i32) -> Result<(), String> {
961        let target_column = column + delta;
962        let original_refs = self
963            .workbook
964            .worksheet(sheet)?
965            .column_cell_references(column)?;
966        let mut original_cells = Vec::new();
967        for r in &original_refs {
968            let cell = self
969                .workbook
970                .worksheet(sheet)?
971                .cell(r.row, column)
972                .ok_or("Expected Cell to exist")?;
973            let style_idx = cell.get_style();
974            let formula_or_value =
975                self.get_cell_formula(sheet, r.row, column)?
976                    .unwrap_or_else(|| {
977                        cell.get_localized_text(
978                            &self.workbook.shared_strings,
979                            self.locale,
980                            self.language,
981                        )
982                    });
983
984            let mut array = None;
985
986            match cell {
987                Cell::EmptyCell { .. }
988                | Cell::BooleanCell { .. }
989                | Cell::NumberCell { .. }
990                | Cell::ErrorCell { .. }
991                | Cell::SharedString { .. }
992                | Cell::CellFormula { .. } => {
993                    // This is a regular cell, we can just move it.
994                }
995                Cell::SpillCell { .. } => {
996                    // This the spill of an array formula. Because dynamic arrays spills have been deleted
997                    // We delete the spill
998                    let worksheet = self.workbook.worksheet_mut(sheet)?;
999                    worksheet.remove_cell(r.row, column)?;
1000                    continue;
1001                }
1002                Cell::ArrayFormula {
1003                    r,
1004                    kind: ArrayKind::Dynamic,
1005                    ..
1006                } => {
1007                    // We are moving the anchor of a dynamic formula.
1008                    // We assume the spill has been taken care of by the caller
1009                    debug_assert_eq!(*r, (1, 1));
1010                }
1011                Cell::ArrayFormula {
1012                    r,
1013                    kind: ArrayKind::Cse,
1014                    ..
1015                } => {
1016                    // This is an array formula, we need to move the whole range
1017                    // We rely on the calling function to check that the move is valid and does not split the array formula
1018                    array = Some(*r);
1019                }
1020            }
1021
1022            original_cells.push((r.row, formula_or_value, style_idx, array));
1023            let ws = self.workbook.worksheet_mut(sheet)?;
1024            ws.remove_cell(r.row, column)?;
1025        }
1026        let width = self
1027            .workbook
1028            .worksheet(sheet)?
1029            .get_actual_column_width(column)?;
1030        let style = self.workbook.worksheet(sheet)?.get_column_style(column)?;
1031        let hidden = self.workbook.worksheet(sheet)?.is_column_hidden(column)?;
1032        if delta > 0 {
1033            for c in column + 1..=target_column {
1034                let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
1035                for r in refs {
1036                    self.move_cell(sheet, r.row, c, r.row, c - 1)?;
1037                }
1038                let w = self.workbook.worksheet(sheet)?.get_actual_column_width(c)?;
1039                let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
1040                let h = self.workbook.worksheet(sheet)?.is_column_hidden(c)?;
1041                self.workbook
1042                    .worksheet_mut(sheet)?
1043                    .set_column_width_and_style(c - 1, w, h, s)?;
1044            }
1045        } else {
1046            for c in (target_column..=column - 1).rev() {
1047                let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
1048                for r in refs {
1049                    self.move_cell(sheet, r.row, c, r.row, c + 1)?;
1050                }
1051                let w = self.workbook.worksheet(sheet)?.get_actual_column_width(c)?;
1052                let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
1053                let h = self.workbook.worksheet(sheet)?.is_column_hidden(c)?;
1054                self.workbook
1055                    .worksheet_mut(sheet)?
1056                    .set_column_width_and_style(c + 1, w, h, s)?;
1057            }
1058        }
1059        for (r, value, style_idx, array) in original_cells {
1060            if let Some(a) = array {
1061                self.set_user_array_formula(sheet, r, target_column, a.0, a.1, &value)?;
1062            } else {
1063                self.set_user_input(sheet, r, target_column, value)?;
1064            }
1065            self.workbook
1066                .worksheet_mut(sheet)?
1067                .set_cell_style(r, target_column, style_idx)?;
1068        }
1069        self.workbook
1070            .worksheet_mut(sheet)?
1071            .set_column_width_and_style(target_column, width, hidden, style)?;
1072        let disp = DisplaceData::ColumnMove {
1073            sheet,
1074            column,
1075            delta,
1076        };
1077        self.displace_cells(&disp)?;
1078        self.displace_cf_ranges(sheet, &disp);
1079        Ok(())
1080    }
1081
1082    // Inner row move: no boundary/can check, no spill reset.
1083    fn move_row_unchecked(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
1084        let target_row = row + delta;
1085        let original_cols = self.get_columns_for_row(sheet, row, false)?;
1086        let mut original_cells = Vec::new();
1087        for c in &original_cols {
1088            let cell = self
1089                .workbook
1090                .worksheet(sheet)?
1091                .cell(row, *c)
1092                .ok_or("Expected Cell to exist")?;
1093            let style_idx = cell.get_style();
1094            let formula_or_value = self.get_cell_formula(sheet, row, *c)?.unwrap_or_else(|| {
1095                cell.get_localized_text(&self.workbook.shared_strings, self.locale, self.language)
1096            });
1097            let mut array = None;
1098
1099            match cell {
1100                Cell::EmptyCell { .. }
1101                | Cell::BooleanCell { .. }
1102                | Cell::NumberCell { .. }
1103                | Cell::ErrorCell { .. }
1104                | Cell::SharedString { .. }
1105                | Cell::CellFormula { .. } => {
1106                    // This is a regular cell, we can just move it.
1107                }
1108                Cell::SpillCell { .. } => {
1109                    // This the spill of an array formula. Because dynamic arrays spills have been deleted
1110                    // We delete the spill
1111                    let worksheet = self.workbook.worksheet_mut(sheet)?;
1112                    worksheet.remove_cell(row, *c)?;
1113                    continue;
1114                }
1115                Cell::ArrayFormula {
1116                    r,
1117                    kind: ArrayKind::Dynamic,
1118                    ..
1119                } => {
1120                    // We are moving the anchor of a dynamic formula.
1121                    // We assume the spill has been taken care of by the caller
1122                    debug_assert_eq!(*r, (1, 1));
1123                }
1124                Cell::ArrayFormula {
1125                    r,
1126                    kind: ArrayKind::Cse,
1127                    ..
1128                } => {
1129                    // This is an array formula, we need to move the whole range
1130                    // We rely on the calling function to check that the move is valid and does not split the array formula
1131                    array = Some(*r);
1132                }
1133            }
1134            original_cells.push((*c, formula_or_value, style_idx, array));
1135            let ws = self.workbook.worksheet_mut(sheet)?;
1136            ws.remove_cell(row, *c)?;
1137        }
1138        if delta > 0 {
1139            for r in row + 1..=target_row {
1140                let cols = self.get_columns_for_row(sheet, r, false)?;
1141                for c in cols {
1142                    self.move_cell(sheet, r, c, r - 1, c)?;
1143                }
1144            }
1145        } else {
1146            for r in (target_row..=row - 1).rev() {
1147                let cols = self.get_columns_for_row(sheet, r, false)?;
1148                for c in cols {
1149                    self.move_cell(sheet, r, c, r + 1, c)?;
1150                }
1151            }
1152        }
1153        for (c, value, style_idx, array) in original_cells {
1154            if let Some(array_range) = array {
1155                self.set_user_array_formula(
1156                    sheet,
1157                    target_row,
1158                    c,
1159                    array_range.0,
1160                    array_range.1,
1161                    &value,
1162                )?;
1163            } else {
1164                self.set_user_input(sheet, target_row, c, value)?;
1165            }
1166            self.workbook
1167                .worksheet_mut(sheet)?
1168                .set_cell_style(target_row, c, style_idx)?;
1169        }
1170        let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
1171        let mut new_rows = Vec::new();
1172        for r in worksheet.rows.iter() {
1173            if r.r == row {
1174                let mut nr = r.clone();
1175                nr.r = target_row;
1176                new_rows.push(nr);
1177            } else if delta > 0 && r.r > row && r.r <= target_row {
1178                let mut nr = r.clone();
1179                nr.r -= 1;
1180                new_rows.push(nr);
1181            } else if delta < 0 && r.r < row && r.r >= target_row {
1182                let mut nr = r.clone();
1183                nr.r += 1;
1184                new_rows.push(nr);
1185            } else {
1186                new_rows.push(r.clone());
1187            }
1188        }
1189        worksheet.rows = new_rows;
1190        let disp = DisplaceData::RowMove { sheet, row, delta };
1191        self.displace_cells(&disp)?;
1192        self.displace_cf_ranges(sheet, &disp);
1193        Ok(())
1194    }
1195
1196    // Returns true if moving columns [column, column+column_count-1] by delta would not
1197    // split any CSE array formula. A formula is OK if its column span is fully within
1198    // the moved group, fully within the displaced zone, or fully outside both.
1199    fn can_move_columns_action(
1200        &self,
1201        sheet: u32,
1202        column: i32,
1203        column_count: i32,
1204        delta: i32,
1205    ) -> Result<bool, String> {
1206        if delta == 0 {
1207            return Ok(true);
1208        }
1209
1210        let group_start = column;
1211        let group_end = column + column_count - 1;
1212
1213        let (displace_start, displace_end) = if delta > 0 {
1214            (group_end + 1, group_end + delta)
1215        } else {
1216            (group_start + delta, group_start - 1)
1217        };
1218
1219        let overlaps = |a_start: i32, a_end: i32, b_start: i32, b_end: i32| {
1220            a_start <= b_end && b_start <= a_end
1221        };
1222
1223        let contains = |a_start: i32, a_end: i32, b_start: i32, b_end: i32| {
1224            a_start <= b_start && b_end <= a_end
1225        };
1226
1227        let interval_is_safe = |array_start: i32, array_end: i32| {
1228            let safe_for = |start: i32, end: i32| {
1229                !overlaps(start, end, array_start, array_end)
1230                    || contains(start, end, array_start, array_end)
1231            };
1232            safe_for(group_start, group_end) && safe_for(displace_start, displace_end)
1233        };
1234
1235        let cell_coords: Vec<(i32, i32)> = {
1236            let worksheet = self.workbook.worksheet(sheet)?;
1237            worksheet
1238                .sheet_data
1239                .iter()
1240                .flat_map(|(r, row_data)| row_data.keys().map(move |c| (*r, *c)))
1241                .collect()
1242        };
1243
1244        for (r, c) in cell_coords {
1245            match self.get_cell_structure(sheet, r, c)? {
1246                CellStructure::ArrayFormula { range } => {
1247                    let (width, _) = range;
1248                    let array_start_col = c;
1249                    let array_end_col = c + width - 1;
1250
1251                    if !interval_is_safe(array_start_col, array_end_col) {
1252                        return Ok(false);
1253                    }
1254                }
1255                CellStructure::SpillArray { anchor, range } => {
1256                    let (width, _) = range;
1257                    let (_, array_start_col) = anchor;
1258                    let array_end_col = array_start_col + width - 1;
1259
1260                    if !interval_is_safe(array_start_col, array_end_col) {
1261                        return Ok(false);
1262                    }
1263                }
1264                _ => {}
1265            }
1266        }
1267
1268        Ok(true)
1269    }
1270
1271    // Returns true if moving rows [row, row+row_count-1] by delta would not
1272    // split any CSE array formula.
1273    // That could happen because:
1274    // * rows are moved in the middle of an array formula
1275    // * we move part of an array
1276    fn can_move_rows_action(
1277        &self,
1278        sheet: u32,
1279        row: i32,
1280        row_count: i32,
1281        delta: i32,
1282    ) -> Result<bool, String> {
1283        if delta == 0 {
1284            return Ok(true);
1285        }
1286
1287        let group_start = row;
1288        let group_end = row + row_count - 1;
1289
1290        let (displace_start, displace_end) = if delta > 0 {
1291            (group_end + 1, group_end + delta)
1292        } else {
1293            (group_start + delta, group_start - 1)
1294        };
1295
1296        let overlaps = |a_start: i32, a_end: i32, b_start: i32, b_end: i32| {
1297            a_start <= b_end && b_start <= a_end
1298        };
1299
1300        let contains = |a_start: i32, a_end: i32, b_start: i32, b_end: i32| {
1301            a_start <= b_start && b_end <= a_end
1302        };
1303
1304        let interval_is_safe = |array_start: i32, array_end: i32| {
1305            let safe_for = |start: i32, end: i32| {
1306                !overlaps(start, end, array_start, array_end)
1307                    || contains(start, end, array_start, array_end)
1308            };
1309
1310            safe_for(group_start, group_end) && safe_for(displace_start, displace_end)
1311        };
1312
1313        // list of all the cells in the sheet
1314        let cell_coords: Vec<(i32, i32)> = {
1315            let worksheet = self.workbook.worksheet(sheet)?;
1316            worksheet
1317                .sheet_data
1318                .iter()
1319                .flat_map(|(r, row_data)| row_data.keys().map(move |c| (*r, *c)))
1320                .collect()
1321        };
1322
1323        for (r, c) in cell_coords {
1324            match self.get_cell_structure(sheet, r, c)? {
1325                CellStructure::ArrayFormula { range } => {
1326                    let (_, height) = range;
1327                    let array_start_row = r;
1328                    let array_end_row = r + height - 1;
1329
1330                    if !interval_is_safe(array_start_row, array_end_row) {
1331                        return Ok(false);
1332                    }
1333                }
1334                CellStructure::SpillArray { anchor, range } => {
1335                    let (_, height) = range;
1336                    let (array_start_row, _) = anchor;
1337                    let array_end_row = array_start_row + height - 1;
1338
1339                    if !interval_is_safe(array_start_row, array_end_row) {
1340                        return Ok(false);
1341                    }
1342                }
1343                _ => {}
1344            }
1345        }
1346
1347        Ok(true)
1348    }
1349
1350    /// Moves a group of columns [column, column+column_count-1] by delta positions.
1351    /// CSE array formulas fully within the moved group are preserved as arrays.
1352    /// Displaces cells due to a move column action
1353    /// from initial_column to target_column = initial_column + column_delta
1354    /// References will be updated following:
1355    /// Cell references:
1356    ///    * All cell references to initial_column will go to target_column
1357    ///    * All cell references to columns in between (initial_column, target_column] will be displaced one to the left
1358    ///    * All other cell references are left unchanged
1359    ///      Ranges. This is the tricky bit:
1360    ///    * Column is one of the extremes of the range. The new extreme would be target_column.
1361    ///      Range is then normalized
1362    ///    * Any other case, range is left unchanged.
1363    ///      NOTE: This moves the data and column styles along with the formulas
1364    pub fn move_columns_action(
1365        &mut self,
1366        sheet: u32,
1367        column: i32,
1368        column_count: i32,
1369        delta: i32,
1370    ) -> Result<(), String> {
1371        if column_count <= 0 || delta == 0 {
1372            return Ok(());
1373        }
1374        let target_first = column + delta;
1375        let target_last = column + column_count - 1 + delta;
1376        if !(1..=LAST_COLUMN).contains(&target_first) || !(1..=LAST_COLUMN).contains(&target_last) {
1377            return Err("Target column out of boundaries".to_string());
1378        }
1379        if !(1..=LAST_COLUMN).contains(&column)
1380            || !(1..=LAST_COLUMN).contains(&(column + column_count - 1))
1381        {
1382            return Err("Initial column out of boundaries".to_string());
1383        }
1384        if !self.can_move_columns_action(sheet, column, column_count, delta)? {
1385            return Err(
1386                "Cannot move columns because that would split an array formula".to_string(),
1387            );
1388        }
1389        self.reset_dynamic_array_spills(sheet)?;
1390
1391        // Move columns in the correct order
1392        if delta > 0 {
1393            for col in (column..column + column_count).rev() {
1394                self.move_column_unchecked(sheet, col, delta)?;
1395            }
1396        } else {
1397            for col in column..column + column_count {
1398                self.move_column_unchecked(sheet, col, delta)?;
1399            }
1400        }
1401
1402        Ok(())
1403    }
1404
1405    /// Displaces cells due to a move row action
1406    /// from initial_row to target_row = initial_row + row_delta
1407    /// References will be updated following the same rules as move_column_action
1408    /// NOTE: This moves the data and row styles along with the formulas
1409    /// Moves a group of rows [row, row+row_count-1] by delta positions.
1410    /// CSE array formulas fully within the moved group are preserved as arrays.
1411    pub fn move_rows_action(
1412        &mut self,
1413        sheet: u32,
1414        row: i32,
1415        row_count: i32,
1416        delta: i32,
1417    ) -> Result<(), String> {
1418        if row_count <= 0 || delta == 0 {
1419            return Ok(());
1420        }
1421        let target_first = row + delta;
1422        let target_last = row + row_count - 1 + delta;
1423        if !(1..=LAST_ROW).contains(&target_first) || !(1..=LAST_ROW).contains(&target_last) {
1424            return Err("Target row out of boundaries".to_string());
1425        }
1426        if !(1..=LAST_ROW).contains(&row) || !(1..=LAST_ROW).contains(&(row + row_count - 1)) {
1427            return Err("Initial row out of boundaries".to_string());
1428        }
1429        if !self.can_move_rows_action(sheet, row, row_count, delta)? {
1430            return Err("Cannot move rows because that would split an array formula".to_string());
1431        }
1432        self.reset_dynamic_array_spills(sheet)?;
1433
1434        // Move rows in the correct order
1435        if delta > 0 {
1436            for r in (row..row + row_count).rev() {
1437                self.move_row_unchecked(sheet, r, delta)?;
1438            }
1439        } else {
1440            for r in row..row + row_count {
1441                self.move_row_unchecked(sheet, r, delta)?;
1442            }
1443        }
1444        Ok(())
1445    }
1446}