Skip to main content

ironcalc_base/
new_empty.rs

1use chrono::DateTime;
2
3use std::collections::HashMap;
4
5use crate::{
6    calc_result::Range,
7    constants::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH},
8    expressions::{
9        lexer::LexerMode,
10        parser::{
11            static_analysis::run_static_analysis_on_node,
12            stringify::{
13                rename_sheet_in_node, to_english_string, to_localized_string, to_rc_format,
14            },
15            Node, Parser,
16        },
17        types::CellReferenceRC,
18    },
19    language::{get_default_language, get_language},
20    locale::{get_default_locale, get_locale},
21    model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
22    types::{
23        DefinedName, Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet,
24        WorksheetView,
25    },
26    utils::ParsedReference,
27};
28
29use crate::tz::Tz;
30
31pub const APPLICATION: &str = "IronCalc Sheets";
32pub const APP_VERSION: &str = "10.0000";
33pub const IRONCALC_USER: &str = "IronCalc User";
34
35/// Name cannot be blank, must be shorter than 31 characters.
36/// You can use all alphanumeric characters but not the following special characters:
37/// \ , / , * , ? , : , [ , ].
38fn is_valid_sheet_name(name: &str) -> bool {
39    let invalid = ['\\', '/', '*', '?', ':', '[', ']'];
40    !name.is_empty() && name.chars().count() <= 31 && !name.contains(&invalid[..])
41}
42
43impl<'a> Model<'a> {
44    /// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists
45    fn new_empty_worksheet(name: &str, sheet_id: u32, view_ids: &[&u32]) -> Worksheet {
46        let mut views = HashMap::new();
47        for id in view_ids {
48            views.insert(
49                **id,
50                WorksheetView {
51                    row: 1,
52                    column: 1,
53                    range: [1, 1, 1, 1],
54                    top_row: 1,
55                    left_column: 1,
56                },
57            );
58        }
59        Worksheet {
60            cols: vec![],
61            rows: vec![],
62            comments: vec![],
63            dimension: "A1".to_string(),
64            merge_cells: vec![],
65            name: name.to_string(),
66            shared_formulas: vec![],
67            sheet_data: Default::default(),
68            sheet_id,
69            state: SheetState::Visible,
70            color: Default::default(),
71            frozen_columns: 0,
72            frozen_rows: 0,
73            show_grid_lines: true,
74            views,
75            conditional_formatting: vec![],
76        }
77    }
78
79    pub fn get_new_sheet_id(&self) -> u32 {
80        let mut index = 1;
81        let worksheets = &self.workbook.worksheets;
82        for worksheet in worksheets {
83            index = index.max(worksheet.sheet_id);
84        }
85        index + 1
86    }
87
88    // This function parses all the internal formulas in all the worksheets
89    // (in the default language ("en") and locale ("en") and the RC format)
90    pub(crate) fn parse_formulas(&mut self) {
91        let locale = self.locale;
92        let language = self.language;
93
94        self.parser.set_locale(get_default_locale());
95        self.parser.set_language(get_default_language());
96        self.parser.set_lexer_mode(LexerMode::R1C1);
97        let worksheets = &self.workbook.worksheets;
98        for worksheet in worksheets {
99            let shared_formulas = &worksheet.shared_formulas;
100            let cell_reference = CellReferenceRC {
101                sheet: worksheet.get_name(),
102                row: 1,
103                column: 1,
104            };
105            let mut parse_formula = Vec::new();
106            for formula in shared_formulas {
107                let t = self.parser.parse(formula, &cell_reference);
108                let static_result = run_static_analysis_on_node(&t);
109                parse_formula.push((t, static_result));
110            }
111            self.parsed_formulas.push(parse_formula);
112        }
113        self.parser.set_lexer_mode(LexerMode::A1);
114        self.parser.set_locale(locale);
115        self.parser.set_language(language);
116    }
117
118    pub(crate) fn parse_defined_names(&mut self) {
119        // Collect first to avoid borrow conflicts when calling self.parser below.
120        let entries: Vec<(String, String, Option<u32>)> = self
121            .workbook
122            .defined_names
123            .iter()
124            .map(|dn| (dn.name.clone(), dn.formula.clone(), dn.sheet_id))
125            .collect();
126
127        let mut parsed_defined_names = HashMap::new();
128
129        for (name, formula, sheet_id) in entries {
130            let parsed_defined_name_formula = if let Ok(reference) =
131                ParsedReference::parse_reference_formula(None, &formula, self.locale, |n| {
132                    self.get_sheet_index_by_name(n)
133                }) {
134                match reference {
135                    ParsedReference::CellReference(cell_reference) => {
136                        ParsedDefinedName::CellReference(cell_reference)
137                    }
138                    ParsedReference::Range(left, right) => {
139                        ParsedDefinedName::RangeReference(Range { left, right })
140                    }
141                }
142            } else {
143                // Try the full parser — the formula might be a LAMBDA definition.
144                // Defined-name formulas may carry a leading '='; strip it before parsing.
145                let formula_body = formula.strip_prefix('=').unwrap_or(&formula);
146                let dummy_ref = CellReferenceRC {
147                    sheet: self
148                        .workbook
149                        .worksheets
150                        .first()
151                        .map(|ws| ws.get_name())
152                        .unwrap_or_else(|| "Sheet1".to_string()),
153                    row: 1,
154                    column: 1,
155                };
156                // Defined-name formulas are stored internally in English, so
157                // they must be parsed with the English parser regardless of the
158                // user's active language.
159                match self.parse_internal_formula(formula_body, &dummy_ref) {
160                    Node::LambdaDefKind { parameters, body } => {
161                        ParsedDefinedName::LambdaDefinition(parameters, *body)
162                    }
163                    _ => ParsedDefinedName::InvalidDefinedNameFormula,
164                }
165            };
166
167            let local_sheet_index = if let Some(sid) = sheet_id {
168                if let Some(idx) = self.get_sheet_index_by_sheet_id(sid) {
169                    Some(idx)
170                } else {
171                    // Sheet with given sheet_id not found.
172                    continue;
173                }
174            } else {
175                None
176            };
177
178            parsed_defined_names.insert(
179                (local_sheet_index, name.to_lowercase()),
180                parsed_defined_name_formula,
181            );
182        }
183
184        self.parsed_defined_names = parsed_defined_names;
185    }
186
187    /// Reparses all formulas and defined names
188    pub(crate) fn reset_parsed_structures(&mut self) {
189        let defined_names = self.workbook.get_defined_names_with_scope();
190        self.parser
191            .set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names);
192        self.parsed_formulas = vec![];
193        self.parse_formulas();
194        self.parsed_defined_names = HashMap::new();
195        self.parse_defined_names();
196        self.evaluate();
197    }
198
199    /// Gets the base name for new sheets
200    fn get_sheet_name(&self) -> String {
201        let language = self.language;
202        match language.code.as_str() {
203            "en" => "Sheet".to_string(),
204            "es" => "Hoja".to_string(),
205            "fr" => "Feuil".to_string(),
206            "de" => "Tabelle".to_string(),
207            "it" => "Foglio".to_string(),
208            _ => "Sheet".to_string(),
209        }
210    }
211
212    /// Adds a sheet with a automatically generated name
213    pub fn new_sheet(&mut self) -> (String, u32) {
214        // First we find a name
215        let base_name = self.get_sheet_name();
216        let base_name_uppercase = base_name.to_uppercase();
217        let mut index = 1;
218        while self
219            .workbook
220            .get_worksheet_names()
221            .iter()
222            .map(|s| s.to_uppercase())
223            .any(|x| x == format!("{base_name_uppercase}{index}"))
224        {
225            index += 1;
226        }
227        let sheet_name = format!("{base_name}{index}");
228        // Now we need a sheet_id
229        let sheet_id = self.get_new_sheet_id();
230        let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
231        let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id, &view_ids);
232        self.workbook.worksheets.push(worksheet);
233        self.reset_parsed_structures();
234        (sheet_name, self.workbook.worksheets.len() as u32 - 1)
235    }
236
237    /// Inserts a sheet with a particular index
238    /// Fails if a worksheet with that name already exists or the name is invalid
239    /// Fails if the index is too large
240    pub fn insert_sheet(
241        &mut self,
242        sheet_name: &str,
243        sheet_index: u32,
244        sheet_id: Option<u32>,
245    ) -> Result<(), String> {
246        if !is_valid_sheet_name(sheet_name) {
247            return Err(format!("Invalid name for a sheet: '{sheet_name}'"));
248        }
249        if self
250            .workbook
251            .get_worksheet_names()
252            .iter()
253            .map(|s| s.to_uppercase())
254            .any(|x| x == sheet_name.to_uppercase())
255        {
256            return Err("A worksheet already exists with that name".to_string());
257        }
258        let sheet_id = match sheet_id {
259            Some(id) => id,
260            None => self.get_new_sheet_id(),
261        };
262        let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
263        let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id, &view_ids);
264        if sheet_index as usize > self.workbook.worksheets.len() {
265            return Err("Sheet index out of range".to_string());
266        }
267        self.workbook
268            .worksheets
269            .insert(sheet_index as usize, worksheet);
270        self.reset_parsed_structures();
271        Ok(())
272    }
273
274    /// Adds a sheet with a specific name
275    /// Fails if a worksheet with that name already exists or the name is invalid
276    pub fn add_sheet(&mut self, sheet_name: &str) -> Result<(), String> {
277        self.insert_sheet(sheet_name, self.workbook.worksheets.len() as u32, None)
278    }
279
280    /// Duplicates an existing sheet, placing the copy immediately after the
281    /// original. Returns the new sheet's name and index.
282    ///
283    /// The new sheet is named `"{original} ({n})"`, where `n` is the smallest
284    /// positive integer that makes the name unique (so `Sheet1` becomes
285    /// `Sheet1 (1)`, then `Sheet1 (2)`, ...).
286    ///
287    /// All the cell data, styles, conditional formatting rules, the sheet tab
288    /// color and the view state are copied. Formulas are copied too:
289    ///   * references to the source sheet itself (whether implicit, like `A1`,
290    ///     or explicit, like `Sheet1!A1`) are retargeted to the new sheet, and
291    ///   * references to other sheets are left untouched.
292    ///
293    /// When copying a sheet:
294    ///   * names local to the source sheet are duplicated as names local to the
295    ///     new sheet, and
296    ///   * global names that reference the source sheet get a new sheet-local
297    ///     copy on the new sheet (the original global name is kept unchanged).
298    ///
299    /// In both cases references to the source sheet are retargeted to the copy.
300    ///
301    /// Fails if `source_index` is out of range.
302    pub fn duplicate_sheet(&mut self, source_index: u32) -> Result<(String, u32), String> {
303        // Validate the source and capture what we need before mutating anything.
304        let source = self.workbook.worksheet(source_index)?;
305        let source_name = source.get_name();
306        let source_sheet_id = source.sheet_id;
307
308        // Find a unique name of the form "{source_name} ({index})". Sheet names
309        // are capped at 31 characters (see `is_valid_sheet_name`), so when the
310        // base name plus the suffix would overflow we truncate the base to make
311        // room — and we still validate every candidate before accepting it,
312        // since we insert the worksheet directly without going through
313        // `insert_sheet`.
314        let existing_names: Vec<String> = self
315            .workbook
316            .get_worksheet_names()
317            .iter()
318            .map(|s| s.to_uppercase())
319            .collect();
320        const MAX_SHEET_NAME_LEN: usize = 31;
321        let mut index = 1;
322        let new_name = loop {
323            let suffix = format!(" ({index})");
324            let suffix_len = suffix.chars().count();
325            // Truncate the base name (by characters, to avoid splitting a
326            // multi-byte char) so that base + suffix fits within the limit.
327            let base: String = if source_name.chars().count() + suffix_len > MAX_SHEET_NAME_LEN {
328                source_name
329                    .chars()
330                    .take(MAX_SHEET_NAME_LEN.saturating_sub(suffix_len))
331                    .collect()
332            } else {
333                source_name.clone()
334            };
335            let candidate = format!("{base}{suffix}");
336            if is_valid_sheet_name(&candidate)
337                && !existing_names.contains(&candidate.to_uppercase())
338            {
339                break candidate;
340            }
341            index += 1;
342        };
343
344        let new_sheet_id = self.get_new_sheet_id();
345
346        // Clone the worksheet wholesale: this brings over cells, styles, merge
347        // cells, comments, conditional formatting, the tab color, frozen panes,
348        // the views and the shared formulas.
349        let mut new_worksheet = self.workbook.worksheet(source_index)?.clone();
350        new_worksheet.name = new_name.clone();
351        new_worksheet.sheet_id = new_sheet_id;
352
353        // Retarget the copied formulas: references to the source sheet become
354        // references to the new sheet, everything else is left as-is. Implicit
355        // (same-sheet) references carry no sheet name, so they automatically
356        // point to whichever sheet hosts the formula — the new sheet.
357        //
358        // Internal formulas are R1C1 and not anchored to a cell; we parse them
359        // in the context of the *source* sheet (the parser already knows that
360        // name) so that implicit references resolve to the source sheet index.
361        self.parser.set_lexer_mode(LexerMode::R1C1);
362        let cell_reference = CellReferenceRC {
363            sheet: source_name.clone(),
364            row: 1,
365            column: 1,
366        };
367        let mut shared_formulas = Vec::with_capacity(new_worksheet.shared_formulas.len());
368        for formula in &new_worksheet.shared_formulas {
369            let mut t = self.parser.parse(formula, &cell_reference);
370            rename_sheet_in_node(&mut t, source_index, &new_name);
371            shared_formulas.push(to_rc_format(&t));
372        }
373        new_worksheet.shared_formulas = shared_formulas;
374        self.parser.set_lexer_mode(LexerMode::A1);
375
376        // Insert the copy right after the source sheet.
377        let new_index = source_index as usize + 1;
378        self.workbook.worksheets.insert(new_index, new_worksheet);
379
380        // Duplicate the relevant defined names as sheet-local names on the copy.
381        // We snapshot first to avoid borrowing the workbook while parsing.
382        let context = self.defined_name_context();
383        let defined_names = self.workbook.defined_names.clone();
384        // A name can exist both as a sheet-local (to the source) and a global
385        // definition. Name resolution prefers the sheet-local one (see
386        // `Parser::get_defined_name`), so the copy must inherit that same
387        // definition. Process local-to-source names first; combined with the
388        // de-dup below this makes the sheet-local definition win regardless of
389        // their order in `workbook.defined_names`. (`sort_by_key` is stable, so
390        // entries within each group keep their original order.)
391        let mut ordered: Vec<&DefinedName> = defined_names.iter().collect();
392        ordered.sort_by_key(|dn| dn.sheet_id != Some(source_sheet_id));
393        let mut new_defined_names: Vec<DefinedName> = Vec::new();
394        for defined_name in ordered {
395            let is_local_to_source = defined_name.sheet_id == Some(source_sheet_id);
396            let is_global = defined_name.sheet_id.is_none();
397            if !is_local_to_source && !is_global {
398                // Local to a different sheet: leave it alone.
399                continue;
400            }
401            // A name may be both global and local-to-source; since
402            // local-to-source entries are processed first, the first match wins
403            // and we skip any later (global) duplicate.
404            if new_defined_names
405                .iter()
406                .any(|d| d.name.eq_ignore_ascii_case(&defined_name.name))
407            {
408                continue;
409            }
410
411            // Defined-name formulas are stored internally in English. Parse,
412            // then retarget references to the source sheet to the copy.
413            let had_equals = defined_name.formula.trim_start().starts_with('=');
414            let body = defined_name
415                .formula
416                .strip_prefix('=')
417                .unwrap_or(&defined_name.formula);
418            let mut node = self.parse_internal_formula(body, &context);
419            let before = to_english_string(&node, &context);
420            rename_sheet_in_node(&mut node, source_index, &new_name);
421            let after = to_english_string(&node, &context);
422
423            // Global names are only duplicated when they actually reference the
424            // source sheet (matching Excel). Local names are always duplicated.
425            if is_global && before == after {
426                continue;
427            }
428
429            let formula = if had_equals {
430                format!("={after}")
431            } else {
432                after
433            };
434            new_defined_names.push(DefinedName {
435                name: defined_name.name.clone(),
436                formula,
437                sheet_id: Some(new_sheet_id),
438            });
439        }
440        self.workbook.defined_names.extend(new_defined_names);
441
442        self.reset_parsed_structures();
443        Ok((new_name, new_index as u32))
444    }
445
446    /// Renames a sheet and updates all existing references to that sheet.
447    /// It can fail if:
448    ///   * The original sheet does not exists
449    ///   * The target sheet already exists
450    ///   * The target sheet name is invalid
451    pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> Result<(), String> {
452        if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) {
453            return self.rename_sheet_by_index(sheet_index, new_name);
454        }
455        Err(format!("Could not find sheet {old_name}"))
456    }
457
458    /// Renames a sheet and updates all existing references to that sheet.
459    /// It can fail if:
460    ///   * The original index is out of bounds
461    ///   * The target sheet name already exists
462    ///   * The target sheet name is invalid
463    pub fn rename_sheet_by_index(
464        &mut self,
465        sheet_index: u32,
466        new_name: &str,
467    ) -> Result<(), String> {
468        if !is_valid_sheet_name(new_name) {
469            return Err(format!("Invalid name for a sheet: '{new_name}'."));
470        }
471        if let Some(new_index) = self.get_sheet_index_by_name(new_name) {
472            if new_index != sheet_index {
473                return Err(format!("Sheet already exists: '{new_name}'."));
474            }
475        }
476        // Gets the new name and checks that a sheet with that index exists
477        let old_name = self.workbook.worksheet(sheet_index)?.get_name();
478
479        // Parse all formulas with the old name
480        // All internal formulas are R1C1
481        self.parser.set_lexer_mode(LexerMode::R1C1);
482
483        for worksheet in &mut self.workbook.worksheets {
484            // R1C1 formulas are not tied to a cell (but are tied to a cell)
485            let cell_reference = &CellReferenceRC {
486                sheet: worksheet.get_name(),
487                row: 1,
488                column: 1,
489            };
490            let mut formulas = Vec::new();
491            for formula in &worksheet.shared_formulas {
492                let mut t = self.parser.parse(formula, cell_reference);
493                rename_sheet_in_node(&mut t, sheet_index, new_name);
494                formulas.push(to_rc_format(&t));
495            }
496            worksheet.shared_formulas = formulas;
497        }
498
499        // Set the mode back to A1
500        self.parser.set_lexer_mode(LexerMode::A1);
501
502        // We reparse all the defined names formulas
503        let mut defined_names = Vec::new();
504        // Defined names do not have a context, we can use anything
505        let cell_reference = &CellReferenceRC {
506            sheet: old_name.clone(),
507            row: 1,
508            column: 1,
509        };
510        for defined_name in &mut self.workbook.defined_names {
511            let mut t = self.parser.parse(&defined_name.formula, cell_reference);
512            rename_sheet_in_node(&mut t, sheet_index, new_name);
513            let formula = to_localized_string(&t, cell_reference, self.locale, self.language);
514            defined_names.push(DefinedName {
515                name: defined_name.name.clone(),
516                formula,
517                sheet_id: defined_name.sheet_id,
518            });
519        }
520        self.workbook.defined_names = defined_names;
521
522        // Update the name of the worksheet
523        self.workbook.worksheet_mut(sheet_index)?.set_name(new_name);
524        self.reset_parsed_structures();
525        Ok(())
526    }
527
528    /// Deletes a sheet by index. Fails if:
529    ///   * The sheet does not exists
530    ///   * It is the last sheet
531    pub fn delete_sheet(&mut self, sheet_index: u32) -> Result<(), String> {
532        let worksheets = &self.workbook.worksheets;
533        let sheet_count = worksheets.len() as u32;
534        if sheet_count == 1 {
535            return Err("Cannot delete only sheet".to_string());
536        };
537        if sheet_index >= sheet_count {
538            return Err("Sheet index too large".to_string());
539        };
540        self.workbook.worksheets.remove(sheet_index as usize);
541        self.reset_parsed_structures();
542        Ok(())
543    }
544
545    /// Moves the worksheet at `sheet_index` to `new_index`, shifting the other
546    /// sheets to accommodate. The moved worksheet ends up at exactly `new_index`.
547    ///
548    /// Sheet order is only a position in the worksheet vector; formulas key off
549    /// the sheet name (and defined names off the sheet id), so cross-sheet
550    /// references stay valid across a move — `reset_parsed_structures` re-resolves
551    /// every reference by name against the reordered vector.
552    ///
553    /// Fails if either index is out of range. Moving a sheet to its current
554    /// position is a no-op.
555    pub fn move_sheet(&mut self, sheet_index: u32, new_index: u32) -> Result<(), String> {
556        let sheet_count = self.workbook.worksheets.len() as u32;
557        if sheet_index >= sheet_count {
558            return Err("Sheet index too large".to_string());
559        }
560        if new_index >= sheet_count {
561            return Err("Target sheet index too large".to_string());
562        }
563        if sheet_index == new_index {
564            return Ok(());
565        }
566        let worksheet = self.workbook.worksheets.remove(sheet_index as usize);
567        self.workbook
568            .worksheets
569            .insert(new_index as usize, worksheet);
570        self.reset_parsed_structures();
571        Ok(())
572    }
573
574    /// Deletes a sheet by name. Fails if:
575    ///   * The sheet does not exists
576    ///   * It is the last sheet
577    pub fn delete_sheet_by_name(&mut self, name: &str) -> Result<(), String> {
578        if let Some(sheet_index) = self.get_sheet_index_by_name(name) {
579            self.delete_sheet(sheet_index)
580        } else {
581            Err("Sheet not found".to_string())
582        }
583    }
584
585    /// Deletes a sheet by sheet_id. Fails if:
586    ///   * The sheet by sheet_id does not exists
587    ///   * It is the last sheet
588    pub fn delete_sheet_by_sheet_id(&mut self, sheet_id: u32) -> Result<(), String> {
589        if let Some(sheet_index) = self.get_sheet_index_by_sheet_id(sheet_id) {
590            self.delete_sheet(sheet_index)
591        } else {
592            Err("Sheet not found".to_string())
593        }
594    }
595
596    pub(crate) fn get_sheet_index_by_sheet_id(&self, sheet_id: u32) -> Option<u32> {
597        let worksheets = &self.workbook.worksheets;
598        for (index, worksheet) in worksheets.iter().enumerate() {
599            if worksheet.sheet_id == sheet_id {
600                return Some(index as u32);
601            }
602        }
603        None
604    }
605
606    /// Creates a new workbook with one empty sheet
607    pub fn new_empty(
608        name: &'a str,
609        locale_id: &'a str,
610        timezone: &'a str,
611        language_id: &'a str,
612    ) -> Result<Model<'a>, String> {
613        let tz = Tz::parse(timezone)?;
614        let locale = match get_locale(locale_id) {
615            Ok(l) => l,
616            Err(_) => return Err(format!("Invalid locale: {locale_id}")),
617        };
618        let language = match get_language(language_id) {
619            Ok(l) => l,
620            Err(_) => return Err(format!("Invalid language: {language_id}")),
621        };
622
623        let milliseconds = get_milliseconds_since_epoch();
624        let seconds = milliseconds / 1000;
625        let dt = match DateTime::from_timestamp(seconds, 0) {
626            Some(s) => s,
627            None => return Err(format!("Invalid timestamp: {milliseconds}")),
628        };
629        // "2020-08-06T21:20:53Z
630        let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
631
632        let mut views = HashMap::new();
633        views.insert(
634            0,
635            WorkbookView {
636                sheet: 0,
637                window_width: DEFAULT_WINDOW_WIDTH,
638                window_height: DEFAULT_WINDOW_HEIGHT,
639            },
640        );
641
642        let sheet_name = match language.code.as_str() {
643            "en" => "Sheet1".to_string(),
644            "es" => "Hoja1".to_string(),
645            "fr" => "Feuil1".to_string(),
646            "de" => "Tabelle1".to_string(),
647            "it" => "Foglio1".to_string(),
648            _ => "Sheet1".to_string(),
649        };
650
651        // String versions of the locale are added here to simplify the serialize/deserialize logic
652        let workbook = Workbook {
653            shared_strings: vec![],
654            defined_names: vec![],
655            worksheets: vec![Model::new_empty_worksheet(&sheet_name, 1, &[&0])],
656            styles: Default::default(),
657            name: name.to_string(),
658            settings: WorkbookSettings {
659                tz: timezone.to_string(),
660                locale: locale_id.to_string(),
661            },
662            metadata: Metadata {
663                application: APPLICATION.to_string(),
664                app_version: APP_VERSION.to_string(),
665                creator: IRONCALC_USER.to_string(),
666                last_modified_by: IRONCALC_USER.to_string(),
667                created: now.clone(),
668                last_modified: now,
669            },
670            tables: HashMap::new(),
671            views,
672            theme: Default::default(),
673        };
674        let parsed_formulas = Vec::new();
675        let worksheets = &workbook.worksheets;
676        let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
677        let parser = Parser::new(worksheet_names, vec![], HashMap::new(), locale, language);
678        let cells = HashMap::new();
679
680        let mut model = Model {
681            workbook,
682            shared_strings: HashMap::new(),
683            parsed_formulas,
684            parsed_defined_names: HashMap::new(),
685            parser,
686            cells,
687            locale,
688            language,
689            tz,
690            view_id: 0,
691            variable_stack: HashMap::new(),
692            last_variable_id: 0,
693            lambdas: HashMap::new(),
694            last_lambda_id: 0,
695            spill_cells: Vec::new(),
696            support: HashMap::new(),
697            cf_cache: HashMap::new(),
698        };
699        model.parse_formulas();
700        model.evaluate_conditional_formatting();
701        Ok(model)
702    }
703}
704
705#[cfg(test)]
706mod tests {
707    #![allow(clippy::unwrap_used, clippy::expect_used)]
708    use super::*;
709
710    use crate::cf_types::{CfRuleInput, ValueOperator};
711    use crate::test::util::new_empty_model;
712    use crate::types::{Color, Dxf, Fill};
713
714    fn red_fill() -> Dxf {
715        Dxf {
716            font: None,
717            fill: Some(Fill {
718                color: Color::Rgb("#FF0000".to_string()),
719            }),
720            border: None,
721            num_fmt: None,
722            alignment: None,
723        }
724    }
725
726    #[test]
727    fn test_duplicate_sheet_naming() {
728        let mut model = new_empty_model();
729
730        // Sheet1 -> Sheet1 (1)
731        let (name1, index1) = model.duplicate_sheet(0).unwrap();
732        assert_eq!(name1, "Sheet1 (1)");
733        assert_eq!(index1, 1); // inserted right after the source
734
735        // Duplicating Sheet1 again -> Sheet1 (2)
736        let (name2, _) = model.duplicate_sheet(0).unwrap();
737        assert_eq!(name2, "Sheet1 (2)");
738
739        // Duplicating the copy -> Sheet1 (1) (1)
740        let source = model.get_sheet_index_by_name("Sheet1 (1)").unwrap();
741        let (name3, _) = model.duplicate_sheet(source).unwrap();
742        assert_eq!(name3, "Sheet1 (1) (1)");
743
744        // Out of range
745        assert!(model.duplicate_sheet(100).is_err());
746    }
747
748    #[test]
749    fn test_duplicate_sheet_name_respects_length_limit() {
750        let mut model = new_empty_model();
751        // A 31-character name (the maximum allowed).
752        let long_name = "AAAAAAAAAABBBBBBBBBBCCCCCCCCCCD";
753        assert_eq!(long_name.chars().count(), 31);
754        model.rename_sheet_by_index(0, long_name).unwrap();
755
756        // Naively "{name} (1)" would be 35 chars and thus invalid. The base
757        // name must be truncated so the result stays within the limit.
758        let (new_name, new_index) = model.duplicate_sheet(0).unwrap();
759        assert!(is_valid_sheet_name(&new_name));
760        assert!(new_name.chars().count() <= 31);
761        assert!(new_name.ends_with(" (1)"));
762        assert_eq!(
763            model.workbook.worksheet(new_index).unwrap().get_name(),
764            new_name
765        );
766
767        // A second copy still produces a distinct, valid name.
768        let (new_name2, _) = model.duplicate_sheet(0).unwrap();
769        assert!(is_valid_sheet_name(&new_name2));
770        assert!(new_name2.chars().count() <= 31);
771        assert_ne!(new_name2, new_name);
772    }
773
774    #[test]
775    fn test_duplicate_sheet_formulas() {
776        let mut model = new_empty_model();
777        model.set_user_input(0, 1, 1, "10".to_string()).unwrap();
778        model.set_user_input(0, 1, 2, "=A1*2".to_string()).unwrap();
779        model.evaluate();
780
781        let (_, new_index) = model.duplicate_sheet(0).unwrap();
782
783        // The implicit self-reference is preserved and points to the copy.
784        assert_eq!(
785            model.get_cell_formula(new_index, 1, 2).unwrap(),
786            Some("=A1*2".to_string())
787        );
788        assert_eq!(
789            model.get_formatted_cell_value(new_index, 1, 2).unwrap(),
790            "20"
791        );
792
793        // The copy is independent: changing the copy doesn't touch the original.
794        model
795            .set_user_input(new_index, 1, 1, "100".to_string())
796            .unwrap();
797        model.evaluate();
798        assert_eq!(
799            model.get_formatted_cell_value(new_index, 1, 2).unwrap(),
800            "200"
801        );
802        assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "20");
803    }
804
805    #[test]
806    fn test_duplicate_sheet_formulas_to_other_sheets() {
807        let mut model = new_empty_model();
808        model.add_sheet("Other").unwrap();
809        let other = model.get_sheet_index_by_name("Other").unwrap();
810        model.set_user_input(other, 1, 1, "7".to_string()).unwrap();
811
812        // A reference to another sheet, and an explicit self-reference.
813        model
814            .set_user_input(0, 1, 1, "=Other!A1".to_string())
815            .unwrap();
816        model.set_user_input(0, 2, 1, "42".to_string()).unwrap();
817        model
818            .set_user_input(0, 1, 2, "=Sheet1!A2".to_string())
819            .unwrap();
820        model.evaluate();
821
822        let (_, new_index) = model.duplicate_sheet(0).unwrap();
823
824        // The cross-sheet reference is unchanged.
825        assert_eq!(
826            model.get_cell_formula(new_index, 1, 1).unwrap(),
827            Some("=Other!A1".to_string())
828        );
829        assert_eq!(
830            model.get_formatted_cell_value(new_index, 1, 1).unwrap(),
831            "7"
832        );
833
834        // The explicit self-reference is retargeted to the copy.
835        assert_eq!(
836            model.get_cell_formula(new_index, 1, 2).unwrap(),
837            Some("='Sheet1 (1)'!A2".to_string())
838        );
839        assert_eq!(
840            model.get_formatted_cell_value(new_index, 1, 2).unwrap(),
841            "42"
842        );
843    }
844
845    #[test]
846    fn test_duplicate_sheet_local_defined_names() {
847        let mut model = new_empty_model();
848        model.set_user_input(0, 1, 1, "5".to_string()).unwrap();
849        model
850            .new_defined_name("local_name", Some(0), "Sheet1!$A$1")
851            .unwrap();
852        model.evaluate();
853
854        let (_, new_index) = model.duplicate_sheet(0).unwrap();
855
856        // The local name is duplicated, scoped to the copy, retargeted to it.
857        let names = model.get_defined_name_list();
858        let copy = names
859            .iter()
860            .find(|(name, scope, _)| name == "local_name" && *scope == Some(new_index))
861            .expect("local name should be duplicated on the copy");
862        assert_eq!(copy.2, "'Sheet1 (1)'!$A$1");
863
864        // The original local name is left untouched.
865        assert!(names
866            .iter()
867            .any(|(name, scope, formula)| name == "local_name"
868                && *scope == Some(0)
869                && formula == "Sheet1!$A$1"));
870    }
871
872    #[test]
873    fn test_duplicate_sheet_global_names_made_local() {
874        let mut model = new_empty_model();
875        model.add_sheet("Other").unwrap();
876        model.set_user_input(0, 1, 1, "5".to_string()).unwrap();
877        // A global name referencing the source sheet, and one that doesn't.
878        model
879            .new_defined_name("from_source", None, "Sheet1!$A$1")
880            .unwrap();
881        model
882            .new_defined_name("from_other", None, "Other!$A$1")
883            .unwrap();
884        model.evaluate();
885
886        let (_, new_index) = model.duplicate_sheet(0).unwrap();
887        let names = model.get_defined_name_list();
888
889        // The original global names are preserved.
890        assert!(names
891            .iter()
892            .any(|(name, scope, _)| name == "from_source" && scope.is_none()));
893        assert!(names
894            .iter()
895            .any(|(name, scope, _)| name == "from_other" && scope.is_none()));
896
897        // The global name referencing the source is duplicated as a sheet-local
898        // name on the copy, retargeted to it.
899        let copy = names
900            .iter()
901            .find(|(name, scope, _)| name == "from_source" && *scope == Some(new_index))
902            .expect("global name referencing source should become local on copy");
903        assert_eq!(copy.2, "'Sheet1 (1)'!$A$1");
904
905        // The global name that does not reference the source is NOT duplicated.
906        assert!(!names
907            .iter()
908            .any(|(name, scope, _)| name == "from_other" && *scope == Some(new_index)));
909    }
910
911    #[test]
912    fn test_duplicate_sheet_prefers_local_over_global_name() {
913        // When a name exists both globally and as sheet-local to the source,
914        // resolution prefers the sheet-local one, so the copy must inherit the
915        // sheet-local definition — regardless of which is stored first.
916        let mut model = new_empty_model();
917        // The global is created first (so it comes earlier in `defined_names`).
918        model
919            .new_defined_name("shared", None, "Sheet1!$A$1")
920            .unwrap();
921        model
922            .new_defined_name("shared", Some(0), "Sheet1!$B$2")
923            .unwrap();
924        model.evaluate();
925
926        let (_, new_index) = model.duplicate_sheet(0).unwrap();
927        let names = model.get_defined_name_list();
928
929        // Exactly one "shared" name is local to the copy and it comes from the
930        // sheet-local definition ($B$2), not the global one ($A$1).
931        let local_copies: Vec<_> = names
932            .iter()
933            .filter(|(name, scope, _)| name == "shared" && *scope == Some(new_index))
934            .collect();
935        assert_eq!(local_copies.len(), 1);
936        assert_eq!(local_copies[0].2, "'Sheet1 (1)'!$B$2");
937    }
938
939    #[test]
940    fn test_duplicate_sheet_conditional_formatting() {
941        let mut model = new_empty_model();
942        model.set_user_input(0, 1, 1, "10".to_string()).unwrap();
943        model
944            .add_conditional_formatting(
945                0,
946                "A1:A10",
947                CfRuleInput::CellIs {
948                    operator: ValueOperator::GreaterThan,
949                    formula: "5".to_string(),
950                    formula2: None,
951                    format: red_fill(),
952                    stop_if_true: false,
953                },
954            )
955            .unwrap();
956        model.evaluate();
957
958        let (_, new_index) = model.duplicate_sheet(0).unwrap();
959
960        let source_rules = model.get_conditional_formatting_list(0).unwrap();
961        let copy_rules = model.get_conditional_formatting_list(new_index).unwrap();
962        assert_eq!(copy_rules.len(), 1);
963        assert_eq!(copy_rules[0].range, "A1:A10");
964        assert_eq!(copy_rules[0].cf_rule, source_rules[0].cf_rule);
965    }
966
967    #[test]
968    fn test_duplicate_sheet_color() {
969        let mut model = new_empty_model();
970        let color = Color::Rgb("#123456".to_string());
971        model.set_sheet_color(0, &color).unwrap();
972
973        let (_, new_index) = model.duplicate_sheet(0).unwrap();
974        assert_eq!(model.workbook.worksheet(new_index).unwrap().color, color);
975    }
976
977    #[test]
978    fn test_is_valid_sheet_name() {
979        assert!(is_valid_sheet_name("Sheet1"));
980        assert!(is_valid_sheet_name("Zażółć gęślą jaźń"));
981
982        assert!(is_valid_sheet_name(" "));
983        assert!(!is_valid_sheet_name(""));
984
985        assert!(is_valid_sheet_name("🙈"));
986
987        assert!(is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCD")); // 31
988        assert!(!is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDE")); // 32
989    }
990}