ironcalc_base 0.7.1

Open source spreadsheet engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::stringify::{
    to_localized_string, to_string_displaced, DisplaceData,
};
use crate::expressions::types::CellReferenceRC;
use crate::model::Model;

// NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns
// In Excel if the whole range is deleted then it will substitute for #REF!
// In IronCalc, if one of the edges of the range is deleted will replace the edge with #REF!
// I feel this is unimportant for now.

impl<'a> Model<'a> {
    fn shift_cell_formula(
        &mut self,
        sheet: u32,
        row: i32,
        column: i32,
        displace_data: &DisplaceData,
    ) -> Result<(), String> {
        if let Some(f) = self
            .workbook
            .worksheet(sheet)?
            .cell(row, column)
            .and_then(|c| c.get_formula())
        {
            let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
            let cell_reference = CellReferenceRC {
                sheet: self.workbook.worksheets[sheet as usize].get_name(),
                row,
                column,
            };
            // FIXME: This is not a very performant way if the formula has changed :S.
            let formula = to_localized_string(node, &cell_reference, self.locale, self.language);
            let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
            if formula != formula_displaced {
                self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))?;
            }
        }
        Ok(())
    }
    /// This function iterates over all cells in the model and shifts their formulas according to the displacement data.
    ///
    /// # Arguments
    ///
    /// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude.
    fn displace_cells(&mut self, displace_data: &DisplaceData) -> Result<(), String> {
        let cells = self.get_all_cells();
        for cell in cells {
            self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data)?;
        }
        Ok(())
    }

    /// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order.
    ///
    /// # Arguments
    ///
    /// * `sheet` - The sheet number to retrieve columns from.
    /// * `row` - The row number to retrieve columns for.
    /// * `descending` - If true, the columns are returned in descending order; otherwise, in ascending order.
    ///
    /// # Returns
    ///
    /// This function returns a `Result` containing either:
    /// - `Ok(Vec<i32>)`: A vector of column indices for the specified row, sorted according to the `descending` flag.
    /// - `Err(String)`: An error message if the sheet cannot be found.
    fn get_columns_for_row(
        &self,
        sheet: u32,
        row: i32,
        descending: bool,
    ) -> Result<Vec<i32>, String> {
        let worksheet = self.workbook.worksheet(sheet)?;
        if let Some(row_data) = worksheet.sheet_data.get(&row) {
            let mut columns: Vec<i32> = row_data.keys().copied().collect();
            columns.sort_unstable();
            if descending {
                columns.reverse();
            }
            Ok(columns)
        } else {
            Ok(vec![])
        }
    }

    /// Moves the contents of cell (source_row, source_column) to (target_row, target_column).
    ///
    /// # Arguments
    ///
    /// * `sheet` - The sheet number to retrieve columns from.
    /// * `source_row` - The row index of the cell's current location.
    /// * `source_column` - The column index of the cell's current location.
    /// * `target_row` - The row index of the cell's new location.
    /// * `target_column` - The column index of the cell's new location.
    fn move_cell(
        &mut self,
        sheet: u32,
        source_row: i32,
        source_column: i32,
        target_row: i32,
        target_column: i32,
    ) -> Result<(), String> {
        let source_cell = self
            .workbook
            .worksheet(sheet)?
            .cell(source_row, source_column)
            .ok_or("Expected Cell to exist")?;
        let style = source_cell.get_style();
        // FIXME: we need some user_input getter instead of get_text
        let formula_or_value = self
            .get_cell_formula(sheet, source_row, source_column)?
            .unwrap_or_else(|| {
                source_cell.get_localized_text(
                    &self.workbook.shared_strings,
                    self.locale,
                    self.language,
                )
            });
        self.set_user_input(sheet, target_row, target_column, formula_or_value)?;
        self.workbook
            .worksheet_mut(sheet)?
            .set_cell_style(target_row, target_column, style)?;
        self.cell_clear_all(sheet, source_row, source_column)
    }

    /// Inserts one or more new columns into the model at the specified index.
    ///
    /// This method shifts existing columns to the right to make space for the new columns.
    ///
    /// # Arguments
    ///
    /// * `sheet` - The sheet number to retrieve columns from.
    /// * `column` - The index at which the new columns should be inserted.
    /// * `column_count` - The number of columns to insert.
    pub fn insert_columns(
        &mut self,
        sheet: u32,
        column: i32,
        column_count: i32,
    ) -> Result<(), String> {
        if column_count <= 0 {
            return Err("Cannot add a negative number of cells :)".to_string());
        }
        // check if it is possible:
        let dimensions = self.workbook.worksheet(sheet)?.dimension();
        let last_column = dimensions.max_column + column_count;
        if last_column > LAST_COLUMN {
            return Err(
                "Cannot shift cells because that would delete cells at the end of a row"
                    .to_string(),
            );
        }
        let worksheet = self.workbook.worksheet(sheet)?;
        let all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
        for row in all_rows {
            let sorted_columns = self.get_columns_for_row(sheet, row, true)?;
            for col in sorted_columns {
                if col >= column {
                    self.move_cell(sheet, row, col, row, col + column_count)?;
                } else {
                    // Break because columns are in descending order.
                    break;
                }
            }
        }

        // Update all formulas in the workbook
        self.displace_cells(
            &(DisplaceData::Column {
                sheet,
                column,
                delta: column_count,
            }),
        )?;

        // In the list of columns:
        // * Keep all the columns to the left
        // * Displace all the columns to the right

        let worksheet = &mut self.workbook.worksheet_mut(sheet)?;

        let mut new_columns = Vec::new();
        for col in worksheet.cols.iter_mut() {
            // range under study
            let min = col.min;
            let max = col.max;
            if column > max {
                // If the range under study is to our left, this is a noop
            } else if column <= min {
                // If the range under study is to our right, we displace it
                col.min = min + column_count;
                col.max = max + column_count;
            } else {
                // If the range under study is in the middle we augment it
                col.max = max + column_count;
            }
            new_columns.push(col.clone());
        }
        // TODO: If in a row the cell to the right and left have the same style we should copy it

        worksheet.cols = new_columns;

        Ok(())
    }

    /// Deletes one or more columns from the model starting at the specified index.
    ///
    /// # Arguments
    ///
    /// * `sheet` - The sheet number to retrieve columns from.
    /// * `column` - The index of the first column to delete.
    /// * `count` - The number of columns to delete.
    pub fn delete_columns(
        &mut self,
        sheet: u32,
        column: i32,
        column_count: i32,
    ) -> Result<(), String> {
        if column_count <= 0 {
            return Err("Please use insert columns instead".to_string());
        }
        if !(1..=LAST_COLUMN).contains(&column) {
            return Err(format!("Column number '{column}' is not valid."));
        }
        if column + column_count - 1 > LAST_COLUMN {
            return Err("Cannot delete columns beyond the last column of the sheet".to_string());
        }

        // first column being deleted
        let column_start = column;
        // last column being deleted
        let column_end = column + column_count - 1;

        // Move cells
        let worksheet = &self.workbook.worksheet(sheet)?;
        let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
        // We do not need to do that, but it is safer to eliminate sources of randomness in the algorithm
        all_rows.sort_unstable();

        for r in all_rows {
            let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
            for col in columns {
                if col >= column_start {
                    if col > column_end {
                        self.move_cell(sheet, r, col, r, col - column_count)?;
                    } else {
                        self.cell_clear_all(sheet, r, col)?;
                    }
                }
            }
        }
        // Update all formulas in the workbook

        self.displace_cells(
            &(DisplaceData::Column {
                sheet,
                column,
                delta: -column_count,
            }),
        )?;
        let worksheet = &mut self.workbook.worksheet_mut(sheet)?;

        // deletes all the column styles
        let mut new_columns = Vec::new();
        for col in worksheet.cols.iter_mut() {
            // range under study
            let min = col.min;
            let max = col.max;
            // In the diagram:
            // |xxxxx| range we are studying [min, max]
            // |*****| range we are deleting [column_start, column_end]
            // we are going to split it in three big cases:
            // ----------------|xxxxxxxx|-----------------
            // -----|*****|------------------------------- Case A
            // -------|**********|------------------------ Case B
            // -------------|**************|-------------- Case C
            // ------------------|****|------------------- Case D
            // ---------------------|**********|---------- Case E
            // -----------------------------|*****|------- Case F
            if column_start < min {
                if column_end < min {
                    // Case A
                    // We displace all columns
                    let mut new_column = col.clone();
                    new_column.min = min - column_count;
                    new_column.max = max - column_count;
                    new_columns.push(new_column);
                } else if column_end < max {
                    // Case B
                    // We displace the end
                    let mut new_column = col.clone();
                    new_column.min = column_start;
                    new_column.max = max - column_count;
                    new_columns.push(new_column);
                } else {
                    // Case C
                    // skip this, we are deleting the whole range
                }
            } else if column_start <= max {
                if column_end <= max {
                    // Case D
                    // We displace the end
                    let mut new_column = col.clone();
                    new_column.max = max - column_count;
                    new_columns.push(new_column);
                } else {
                    // Case E
                    let mut new_column = col.clone();
                    new_column.max = column_start - 1;
                    new_columns.push(new_column);
                }
            } else {
                // Case F
                // No action required
                new_columns.push(col.clone());
            }
        }
        worksheet.cols = new_columns;

        Ok(())
    }

    /// Inserts one or more new rows into the model at the specified index.
    ///
    /// # Arguments
    ///
    /// * `sheet` - The sheet number to retrieve columns from.
    /// * `row` - The index at which the new rows should be inserted.
    /// * `row_count` - The number of rows to insert.
    pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
        if row_count <= 0 {
            return Err("Cannot add a negative number of cells :)".to_string());
        }
        // Check if it is possible:
        let dimensions = self.workbook.worksheet(sheet)?.dimension();
        let last_row = dimensions.max_row + row_count;
        if last_row > LAST_ROW {
            return Err(
                "Cannot shift cells because that would delete cells at the end of a column"
                    .to_string(),
            );
        }

        // Move cells
        let worksheet = &self.workbook.worksheet(sheet)?;
        let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
        all_rows.sort_unstable();
        all_rows.reverse();
        for r in all_rows {
            if r >= row {
                // We do not really need the columns in any order
                let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
                for column in columns {
                    self.move_cell(sheet, r, column, r + row_count, column)?;
                }
            } else {
                // Rows are in descending order
                break;
            }
        }
        // In the list of rows styles:
        // * Add all rows above the rows we are inserting unchanged
        // * Shift the ones below
        let rows = &self.workbook.worksheets[sheet as usize].rows;
        let mut new_rows = vec![];
        for r in rows {
            if r.r < row {
                new_rows.push(r.clone());
            } else if r.r >= row {
                let mut new_row = r.clone();
                new_row.r = r.r + row_count;
                new_rows.push(new_row);
            }
        }
        self.workbook.worksheets[sheet as usize].rows = new_rows;

        // Update all formulas in the workbook
        self.displace_cells(
            &(DisplaceData::Row {
                sheet,
                row,
                delta: row_count,
            }),
        )?;

        Ok(())
    }

    /// Deletes one or more rows from the model starting at the specified index.
    ///
    /// # Arguments
    ///
    /// * `sheet` - The sheet number to retrieve columns from.
    /// * `row` - The index of the first row to delete.
    /// * `row_count` - The number of rows to delete.
    pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
        if row_count <= 0 {
            return Err("Please use insert rows instead".to_string());
        }
        if !(1..=LAST_ROW).contains(&row) {
            return Err(format!("Row number '{row}' is not valid."));
        }
        if row + row_count - 1 > LAST_ROW {
            return Err("Cannot delete rows beyond the last row of the sheet".to_string());
        }

        // Move cells
        let worksheet = &self.workbook.worksheet(sheet)?;
        let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
        all_rows.sort_unstable();

        for r in all_rows {
            if r >= row {
                // We do not need ordered, but it is safer to eliminate sources of randomness in the algorithm
                let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
                if r >= row + row_count {
                    // displace all cells in column
                    for column in columns {
                        self.move_cell(sheet, r, column, r - row_count, column)?;
                    }
                } else {
                    // remove all cells in row
                    // FIXME: We could just remove the entire row in one go
                    for column in columns {
                        self.cell_clear_all(sheet, r, column)?;
                    }
                }
            }
        }
        // In the list of rows styles:
        // * Add all rows above the rows we are deleting unchanged
        // * Skip all those we are deleting
        // * Shift the ones below
        let rows = &self.workbook.worksheets[sheet as usize].rows;
        let mut new_rows = vec![];
        for r in rows {
            if r.r < row {
                new_rows.push(r.clone());
            } else if r.r >= row + row_count {
                let mut new_row = r.clone();
                new_row.r = r.r - row_count;
                new_rows.push(new_row);
            }
        }
        self.workbook.worksheets[sheet as usize].rows = new_rows;
        self.displace_cells(
            &(DisplaceData::Row {
                sheet,
                row,
                delta: -row_count,
            }),
        )?;
        Ok(())
    }

    /// Displaces cells due to a move column action
    /// from initial_column to target_column = initial_column + column_delta
    /// References will be updated following:
    /// Cell references:
    ///    * All cell references to initial_column will go to target_column
    ///    * All cell references to columns in between (initial_column, target_column] will be displaced one to the left
    ///    * All other cell references are left unchanged
    ///      Ranges. This is the tricky bit:
    ///    * Column is one of the extremes of the range. The new extreme would be target_column.
    ///      Range is then normalized
    ///    * Any other case, range is left unchanged.
    ///      NOTE: This moves the data and column styles along with the formulas
    pub fn move_column_action(
        &mut self,
        sheet: u32,
        column: i32,
        delta: i32,
    ) -> Result<(), String> {
        // Check boundaries
        let target_column = column + delta;
        if !(1..=LAST_COLUMN).contains(&target_column) {
            return Err("Target column out of boundaries".to_string());
        }
        if !(1..=LAST_COLUMN).contains(&column) {
            return Err("Initial column out of boundaries".to_string());
        }

        if delta == 0 {
            return Ok(());
        }

        // Preserve cell contents, width and style of the column being moved
        let original_refs = self
            .workbook
            .worksheet(sheet)?
            .column_cell_references(column)?;
        let mut original_cells = Vec::new();
        for r in &original_refs {
            let cell = self
                .workbook
                .worksheet(sheet)?
                .cell(r.row, column)
                .ok_or("Expected Cell to exist")?;
            let style_idx = cell.get_style();
            let formula_or_value =
                self.get_cell_formula(sheet, r.row, column)?
                    .unwrap_or_else(|| {
                        cell.get_localized_text(
                            &self.workbook.shared_strings,
                            self.locale,
                            self.language,
                        )
                    });
            original_cells.push((r.row, formula_or_value, style_idx));
            self.cell_clear_all(sheet, r.row, column)?;
        }

        let width = self.workbook.worksheet(sheet)?.get_column_width(column)?;
        let style = self.workbook.worksheet(sheet)?.get_column_style(column)?;

        if delta > 0 {
            for c in column + 1..=target_column {
                let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
                for r in refs {
                    self.move_cell(sheet, r.row, c, r.row, c - 1)?;
                }

                let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
                let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
                self.workbook
                    .worksheet_mut(sheet)?
                    .set_column_width_and_style(c - 1, w, s)?;
            }
        } else {
            for c in (target_column..=column - 1).rev() {
                let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?;
                for r in refs {
                    self.move_cell(sheet, r.row, c, r.row, c + 1)?;
                }

                let w = self.workbook.worksheet(sheet)?.get_column_width(c)?;
                let s = self.workbook.worksheet(sheet)?.get_column_style(c)?;
                self.workbook
                    .worksheet_mut(sheet)?
                    .set_column_width_and_style(c + 1, w, s)?;
            }
        }

        for (r, value, style_idx) in original_cells {
            self.set_user_input(sheet, r, target_column, value)?;
            self.workbook
                .worksheet_mut(sheet)?
                .set_cell_style(r, target_column, style_idx)?;
        }
        self.workbook
            .worksheet_mut(sheet)?
            .set_column_width_and_style(target_column, width, style)?;

        // Update all formulas in the workbook
        self.displace_cells(
            &(DisplaceData::ColumnMove {
                sheet,
                column,
                delta,
            }),
        )?;

        Ok(())
    }

    /// Displaces cells due to a move row action
    /// from initial_row to target_row = initial_row + row_delta
    /// References will be updated following the same rules as move_column_action
    /// NOTE: This moves the data and row styles along with the formulas
    pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> {
        // Check boundaries
        let target_row = row + delta;
        if !(1..=LAST_ROW).contains(&target_row) {
            return Err("Target row out of boundaries".to_string());
        }
        if !(1..=LAST_ROW).contains(&row) {
            return Err("Initial row out of boundaries".to_string());
        }

        if delta == 0 {
            return Ok(());
        }

        let original_cols = self.get_columns_for_row(sheet, row, false)?;
        let mut original_cells = Vec::new();
        for c in &original_cols {
            let cell = self
                .workbook
                .worksheet(sheet)?
                .cell(row, *c)
                .ok_or("Expected Cell to exist")?;
            let style_idx = cell.get_style();
            let formula_or_value = self.get_cell_formula(sheet, row, *c)?.unwrap_or_else(|| {
                cell.get_localized_text(&self.workbook.shared_strings, self.locale, self.language)
            });
            original_cells.push((*c, formula_or_value, style_idx));
            self.cell_clear_all(sheet, row, *c)?;
        }

        if delta > 0 {
            for r in row + 1..=target_row {
                let cols = self.get_columns_for_row(sheet, r, false)?;
                for c in cols {
                    self.move_cell(sheet, r, c, r - 1, c)?;
                }
            }
        } else {
            for r in (target_row..=row - 1).rev() {
                let cols = self.get_columns_for_row(sheet, r, false)?;
                for c in cols {
                    self.move_cell(sheet, r, c, r + 1, c)?;
                }
            }
        }

        for (c, value, style_idx) in original_cells {
            self.set_user_input(sheet, target_row, c, value)?;
            self.workbook
                .worksheet_mut(sheet)?
                .set_cell_style(target_row, c, style_idx)?;
        }

        let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
        let mut new_rows = Vec::new();
        for r in worksheet.rows.iter() {
            if r.r == row {
                let mut nr = r.clone();
                nr.r = target_row;
                new_rows.push(nr);
            } else if delta > 0 && r.r > row && r.r <= target_row {
                let mut nr = r.clone();
                nr.r -= 1;
                new_rows.push(nr);
            } else if delta < 0 && r.r < row && r.r >= target_row {
                let mut nr = r.clone();
                nr.r += 1;
                new_rows.push(nr);
            } else {
                new_rows.push(r.clone());
            }
        }
        worksheet.rows = new_rows;

        // Update all formulas in the workbook
        self.displace_cells(&(DisplaceData::RowMove { sheet, row, delta }))?;

        Ok(())
    }
}