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
35fn 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 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 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 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 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 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 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 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 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 pub fn new_sheet(&mut self) -> (String, u32) {
214 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 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 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 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 pub fn duplicate_sheet(&mut self, source_index: u32) -> Result<(String, u32), String> {
303 let source = self.workbook.worksheet(source_index)?;
305 let source_name = source.get_name();
306 let source_sheet_id = source.sheet_id;
307
308 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 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 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 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 let new_index = source_index as usize + 1;
378 self.workbook.worksheets.insert(new_index, new_worksheet);
379
380 let context = self.defined_name_context();
383 let defined_names = self.workbook.defined_names.clone();
384 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 continue;
400 }
401 if new_defined_names
405 .iter()
406 .any(|d| d.name.eq_ignore_ascii_case(&defined_name.name))
407 {
408 continue;
409 }
410
411 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 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 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 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 let old_name = self.workbook.worksheet(sheet_index)?.get_name();
478
479 self.parser.set_lexer_mode(LexerMode::R1C1);
482
483 for worksheet in &mut self.workbook.worksheets {
484 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 self.parser.set_lexer_mode(LexerMode::A1);
501
502 let mut defined_names = Vec::new();
504 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 self.workbook.worksheet_mut(sheet_index)?.set_name(new_name);
524 self.reset_parsed_structures();
525 Ok(())
526 }
527
528 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 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 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 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 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 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 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 let (name1, index1) = model.duplicate_sheet(0).unwrap();
732 assert_eq!(name1, "Sheet1 (1)");
733 assert_eq!(index1, 1); let (name2, _) = model.duplicate_sheet(0).unwrap();
737 assert_eq!(name2, "Sheet1 (2)");
738
739 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 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 let long_name = "AAAAAAAAAABBBBBBBBBBCCCCCCCCCCD";
753 assert_eq!(long_name.chars().count(), 31);
754 model.rename_sheet_by_index(0, long_name).unwrap();
755
756 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 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 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 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 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 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 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 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 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 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 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 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 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 let mut model = new_empty_model();
917 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 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")); assert!(!is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDE")); }
990}