use chrono::DateTime; use std::collections::HashMap; use crate::{ calc_result::Range, constants::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH}, expressions::{ lexer::LexerMode, parser::{ stringify::{rename_sheet_in_node, to_rc_format}, Parser, }, types::CellReferenceRC, }, language::get_language, locale::get_locale, model::{get_milliseconds_since_epoch, Model, ParsedDefinedName}, types::{ Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView, }, utils::ParsedReference, }; use chrono_tz::Tz; pub const APPLICATION: &str = "IronCalc Sheets"; pub const APP_VERSION: &str = "10.0000"; pub const IRONCALC_USER: &str = "IronCalc User"; /// Name cannot be blank, must be shorter than 31 characters. /// You can use all alphanumeric characters but not the following special characters: /// \ , / , * , ? , : , [ , ]. fn is_valid_sheet_name(name: &str) -> bool { let invalid = ['\\', '/', '*', '?', ':', '[', ']']; !name.is_empty() && name.chars().count() <= 31 && !name.contains(&invalid[..]) } impl Model { /// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists fn new_empty_worksheet(name: &str, sheet_id: u32, view_ids: &[&u32]) -> Worksheet { let mut views = HashMap::new(); for id in view_ids { views.insert( **id, WorksheetView { row: 1, column: 1, range: [1, 1, 1, 1], top_row: 1, left_column: 1, }, ); } Worksheet { cols: vec![], rows: vec![], comments: vec![], dimension: "A1".to_string(), merge_cells: vec![], name: name.to_string(), shared_formulas: vec![], sheet_data: Default::default(), sheet_id, state: SheetState::Visible, color: Default::default(), frozen_columns: 0, frozen_rows: 0, show_grid_lines: true, views, } } pub fn get_new_sheet_id(&self) -> u32 { let mut index = 1; let worksheets = &self.workbook.worksheets; for worksheet in worksheets { index = index.max(worksheet.sheet_id); } index + 1 } pub(crate) fn parse_formulas(&mut self) { self.parser.set_lexer_mode(LexerMode::R1C1); let worksheets = &self.workbook.worksheets; for worksheet in worksheets { let shared_formulas = &worksheet.shared_formulas; let cell_reference = CellReferenceRC { sheet: worksheet.get_name(), row: 1, column: 1, }; let mut parse_formula = Vec::new(); for formula in shared_formulas { let t = self.parser.parse(formula, &cell_reference); parse_formula.push(t); } self.parsed_formulas.push(parse_formula); } self.parser.set_lexer_mode(LexerMode::A1); } pub(crate) fn parse_defined_names(&mut self) { let mut parsed_defined_names = HashMap::new(); for defined_name in &self.workbook.defined_names { let parsed_defined_name_formula = if let Ok(reference) = ParsedReference::parse_reference_formula( None, &defined_name.formula, &self.locale, |name| self.get_sheet_index_by_name(name), ) { match reference { ParsedReference::CellReference(cell_reference) => { ParsedDefinedName::CellReference(cell_reference) } ParsedReference::Range(left, right) => { ParsedDefinedName::RangeReference(Range { left, right }) } } } else { ParsedDefinedName::InvalidDefinedNameFormula }; let local_sheet_index = if let Some(sheet_id) = defined_name.sheet_id { if let Some(sheet_index) = self.get_sheet_index_by_sheet_id(sheet_id) { Some(sheet_index) } else { // TODO: Error: Sheet with given sheet_id not found. continue; } } else { None }; parsed_defined_names.insert( (local_sheet_index, defined_name.name.to_lowercase()), parsed_defined_name_formula, ); } self.parsed_defined_names = parsed_defined_names; } /// Reparses all formulas and defined names pub(crate) fn reset_parsed_structures(&mut self) { let defined_names = self .workbook .get_defined_names_with_scope() .iter() .map(|s| (s.0.to_owned(), s.1)) .collect(); self.parser .set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names); self.parsed_formulas = vec![]; self.parse_formulas(); self.parsed_defined_names = HashMap::new(); self.parse_defined_names(); self.evaluate(); } /// Adds a sheet with a automatically generated name pub fn new_sheet(&mut self) -> (String, u32) { // First we find a name // TODO: The name should depend on the locale let base_name = "Sheet"; let base_name_uppercase = base_name.to_uppercase(); let mut index = 1; while self .workbook .get_worksheet_names() .iter() .map(|s| s.to_uppercase()) .any(|x| x == format!("{}{}", base_name_uppercase, index)) { index += 1; } let sheet_name = format!("{}{}", base_name, index); // Now we need a sheet_id let sheet_id = self.get_new_sheet_id(); let view_ids: Vec<&u32> = self.workbook.views.keys().collect(); let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id, &view_ids); self.workbook.worksheets.push(worksheet); self.reset_parsed_structures(); (sheet_name, self.workbook.worksheets.len() as u32 - 1) } /// Inserts a sheet with a particular index /// Fails if a worksheet with that name already exists or the name is invalid /// Fails if the index is too large pub fn insert_sheet( &mut self, sheet_name: &str, sheet_index: u32, sheet_id: Option, ) -> Result<(), String> { if !is_valid_sheet_name(sheet_name) { return Err(format!("Invalid name for a sheet: '{}'", sheet_name)); } if self .workbook .get_worksheet_names() .iter() .map(|s| s.to_uppercase()) .any(|x| x == sheet_name.to_uppercase()) { return Err("A worksheet already exists with that name".to_string()); } let sheet_id = match sheet_id { Some(id) => id, None => self.get_new_sheet_id(), }; let view_ids: Vec<&u32> = self.workbook.views.keys().collect(); let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id, &view_ids); if sheet_index as usize > self.workbook.worksheets.len() { return Err("Sheet index out of range".to_string()); } self.workbook .worksheets .insert(sheet_index as usize, worksheet); self.reset_parsed_structures(); Ok(()) } /// Adds a sheet with a specific name /// Fails if a worksheet with that name already exists or the name is invalid pub fn add_sheet(&mut self, sheet_name: &str) -> Result<(), String> { self.insert_sheet(sheet_name, self.workbook.worksheets.len() as u32, None) } /// Renames a sheet and updates all existing references to that sheet. /// It can fail if: /// * The original sheet does not exists /// * The target sheet already exists /// * The target sheet name is invalid pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> Result<(), String> { if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) { return self.rename_sheet_by_index(sheet_index, new_name); } Err(format!("Could not find sheet {}", old_name)) } /// Renames a sheet and updates all existing references to that sheet. /// It can fail if: /// * The original index is too large /// * The target sheet name already exists /// * The target sheet name is invalid pub fn rename_sheet_by_index( &mut self, sheet_index: u32, new_name: &str, ) -> Result<(), String> { if !is_valid_sheet_name(new_name) { return Err(format!("Invalid name for a sheet: '{}'.", new_name)); } if self.get_sheet_index_by_name(new_name).is_some() { return Err(format!("Sheet already exists: '{}'.", new_name)); } let worksheets = &self.workbook.worksheets; let sheet_count = worksheets.len() as u32; if sheet_index >= sheet_count { return Err("Sheet index out of bounds".to_string()); } // Parse all formulas with the old name // All internal formulas are R1C1 self.parser.set_lexer_mode(LexerMode::R1C1); // We use iter because the default would be a mut_iter and we don't need a mutable reference let worksheets = &mut self.workbook.worksheets; for worksheet in worksheets { let cell_reference = &CellReferenceRC { sheet: worksheet.get_name(), row: 1, column: 1, }; let mut formulas = Vec::new(); for formula in &worksheet.shared_formulas { let mut t = self.parser.parse(formula, cell_reference); rename_sheet_in_node(&mut t, sheet_index, new_name); formulas.push(to_rc_format(&t)); } worksheet.shared_formulas = formulas; } // Se the mode back to A1 self.parser.set_lexer_mode(LexerMode::A1); // Update the name of the worksheet let worksheets = &mut self.workbook.worksheets; worksheets[sheet_index as usize].set_name(new_name); self.reset_parsed_structures(); Ok(()) } /// Deletes a sheet by index. Fails if: /// * The sheet does not exists /// * It is the last sheet pub fn delete_sheet(&mut self, sheet_index: u32) -> Result<(), String> { let worksheets = &self.workbook.worksheets; let sheet_count = worksheets.len() as u32; if sheet_count == 1 { return Err("Cannot delete only sheet".to_string()); }; if sheet_index >= sheet_count { return Err("Sheet index too large".to_string()); } self.workbook.worksheets.remove(sheet_index as usize); self.reset_parsed_structures(); Ok(()) } /// Deletes a sheet by name. Fails if: /// * The sheet does not exists /// * It is the last sheet pub fn delete_sheet_by_name(&mut self, name: &str) -> Result<(), String> { if let Some(sheet_index) = self.get_sheet_index_by_name(name) { self.delete_sheet(sheet_index) } else { Err("Sheet not found".to_string()) } } /// Deletes a sheet by sheet_id. Fails if: /// * The sheet by sheet_id does not exists /// * It is the last sheet pub fn delete_sheet_by_sheet_id(&mut self, sheet_id: u32) -> Result<(), String> { if let Some(sheet_index) = self.get_sheet_index_by_sheet_id(sheet_id) { self.delete_sheet(sheet_index) } else { Err("Sheet not found".to_string()) } } pub(crate) fn get_sheet_index_by_sheet_id(&self, sheet_id: u32) -> Option { let worksheets = &self.workbook.worksheets; for (index, worksheet) in worksheets.iter().enumerate() { if worksheet.sheet_id == sheet_id { return Some(index as u32); } } None } /// Creates a new workbook with one empty sheet pub fn new_empty(name: &str, locale_id: &str, timezone: &str) -> Result { let tz: Tz = match &timezone.parse() { Ok(tz) => *tz, Err(_) => return Err(format!("Invalid timezone: {}", &timezone)), }; let locale = match get_locale(locale_id) { Ok(l) => l.clone(), Err(_) => return Err(format!("Invalid locale: {}", locale_id)), }; let milliseconds = get_milliseconds_since_epoch(); let seconds = milliseconds / 1000; let dt = match DateTime::from_timestamp(seconds, 0) { Some(s) => s, None => return Err(format!("Invalid timestamp: {}", milliseconds)), }; // "2020-08-06T21:20:53Z let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(); let mut views = HashMap::new(); views.insert( 0, WorkbookView { sheet: 0, window_width: DEFAULT_WINDOW_WIDTH, window_height: DEFAULT_WINDOW_HEIGHT, }, ); // String versions of the locale are added here to simplify the serialize/deserialize logic let workbook = Workbook { shared_strings: vec![], defined_names: vec![], worksheets: vec![Model::new_empty_worksheet("Sheet1", 1, &[&0])], styles: Default::default(), name: name.to_string(), settings: WorkbookSettings { tz: timezone.to_string(), locale: locale_id.to_string(), }, metadata: Metadata { application: APPLICATION.to_string(), app_version: APP_VERSION.to_string(), creator: IRONCALC_USER.to_string(), last_modified_by: IRONCALC_USER.to_string(), created: now.clone(), last_modified: now, }, tables: HashMap::new(), views, }; let parsed_formulas = Vec::new(); let worksheets = &workbook.worksheets; let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); let parser = Parser::new(worksheet_names, vec![], HashMap::new()); let cells = HashMap::new(); // FIXME: Add support for display languages #[allow(clippy::expect_used)] let language = get_language("en").expect("").clone(); let mut model = Model { workbook, shared_strings: HashMap::new(), parsed_formulas, parsed_defined_names: HashMap::new(), parser, cells, locale, language, tz, view_id: 0, }; model.parse_formulas(); Ok(model) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_valid_sheet_name() { assert!(is_valid_sheet_name("Sheet1")); assert!(is_valid_sheet_name("Zażółć gęślą jaźń")); assert!(is_valid_sheet_name(" ")); assert!(!is_valid_sheet_name("")); assert!(is_valid_sheet_name("🙈")); assert!(is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCD")); // 31 assert!(!is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDE")); // 32 } }