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 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 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 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 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 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 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 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 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 pub fn new_sheet(&mut self) -> (String, u32) {
215 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 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 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 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 pub fn duplicate_sheet(&mut self, source_index: u32) -> Result<(String, u32), String> {
304 let source = self.workbook.worksheet(source_index)?;
306 let source_name = source.get_name();
307 let source_sheet_id = source.sheet_id;
308
309 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 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 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 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 let new_index = source_index as usize + 1;
379 self.workbook.worksheets.insert(new_index, new_worksheet);
380
381 let context = self.defined_name_context();
384 let defined_names = self.workbook.defined_names.clone();
385 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 continue;
401 }
402 if new_defined_names
406 .iter()
407 .any(|d| d.name.eq_ignore_ascii_case(&defined_name.name))
408 {
409 continue;
410 }
411
412 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 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 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 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 let old_name = self.workbook.worksheet(sheet_index)?.get_name();
479
480 self.parser.set_lexer_mode(LexerMode::R1C1);
483
484 for worksheet in &mut self.workbook.worksheets {
485 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 self.parser.set_lexer_mode(LexerMode::A1);
502
503 let mut defined_names = Vec::new();
505 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 self.workbook.worksheet_mut(sheet_index)?.set_name(new_name);
525 self.reset_parsed_structures();
526 Ok(())
527 }
528
529 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 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 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 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 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 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 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 let (name1, index1) = model.duplicate_sheet(0).unwrap();
734 assert_eq!(name1, "Sheet1 (1)");
735 assert_eq!(index1, 1); let (name2, _) = model.duplicate_sheet(0).unwrap();
739 assert_eq!(name2, "Sheet1 (2)");
740
741 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 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 let long_name = "AAAAAAAAAABBBBBBBBBBCCCCCCCCCCD";
755 assert_eq!(long_name.chars().count(), 31);
756 model.rename_sheet_by_index(0, long_name).unwrap();
757
758 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 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 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 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 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 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 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 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 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 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 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 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 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 let mut model = new_empty_model();
919 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 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")); assert!(!is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDE")); }
992}