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