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 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 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 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 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 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 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 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 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 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 pub fn new_sheet(&mut self) -> (String, u32) {
222 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 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 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 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 pub fn duplicate_sheet(&mut self, source_index: u32) -> Result<(String, u32), String> {
311 let source = self.workbook.worksheet(source_index)?;
313 let source_name = source.get_name();
314 let source_sheet_id = source.sheet_id;
315
316 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 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 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 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 let new_index = source_index as usize + 1;
386 self.workbook.worksheets.insert(new_index, new_worksheet);
387
388 let context = self.defined_name_context();
391 let defined_names = self.workbook.defined_names.clone();
392 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 continue;
408 }
409 if new_defined_names
413 .iter()
414 .any(|d| d.name.eq_ignore_ascii_case(&defined_name.name))
415 {
416 continue;
417 }
418
419 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 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 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 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 let old_name = self.workbook.worksheet(sheet_index)?.get_name();
486
487 self.parser.set_lexer_mode(LexerMode::R1C1);
490
491 for worksheet in &mut self.workbook.worksheets {
492 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 self.parser.set_lexer_mode(LexerMode::A1);
509
510 let mut defined_names = Vec::new();
512 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 self.workbook.worksheet_mut(sheet_index)?.set_name(new_name);
532 self.reset_parsed_structures();
533 Ok(())
534 }
535
536 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 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 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 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 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 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 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 let (name1, index1) = model.duplicate_sheet(0).unwrap();
739 assert_eq!(name1, "Sheet1 (1)");
740 assert_eq!(index1, 1); let (name2, _) = model.duplicate_sheet(0).unwrap();
744 assert_eq!(name2, "Sheet1 (2)");
745
746 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 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 let long_name = "AAAAAAAAAABBBBBBBBBBCCCCCCCCCCD";
760 assert_eq!(long_name.chars().count(), 31);
761 model.rename_sheet_by_index(0, long_name).unwrap();
762
763 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 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 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 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 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 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 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 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 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 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 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 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 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 let mut model = new_empty_model();
924 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 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")); assert!(!is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDE")); }
997}