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