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