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