use chrono::NaiveDateTime;
use std::collections::HashMap;
use crate::{
calc_result::Range,
expressions::{
lexer::LexerMode,
parser::stringify::{rename_sheet_in_node, to_rc_format},
parser::Parser,
types::CellReferenceRC,
},
language::get_language,
locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{Metadata, SheetState, Workbook, WorkbookSettings, Worksheet},
utils::ParsedReference,
};
pub use chrono_tz::Tz;
pub const APPLICATION: &str = "IronCalc Sheets";
pub const APP_VERSION: &str = "10.0000";
pub const IRONCALC_USER: &str = "IronCalc User";
fn is_valid_sheet_name(name: &str) -> bool {
let invalid = ['\\', '/', '*', '?', ':', '[', ']'];
return !name.is_empty() && name.chars().count() <= 31 && !name.contains(&invalid[..]);
}
impl Model {
fn new_empty_worksheet(name: &str, sheet_id: u32) -> Worksheet {
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,
}
}
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 = &Some(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 {
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;
}
fn reset_parsed_structures(&mut self) {
self.parser
.set_worksheets(self.workbook.get_worksheet_names());
self.parsed_formulas = vec![];
self.parse_formulas();
self.parsed_defined_names = HashMap::new();
self.parse_defined_names();
self.evaluate();
}
pub fn new_sheet(&mut self) {
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);
let sheet_id = self.get_new_sheet_id();
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
self.workbook.worksheets.push(worksheet);
self.reset_parsed_structures();
}
pub fn insert_sheet(
&mut self,
sheet_name: &str,
sheet_index: u32,
sheet_id: Option<u32>,
) -> 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 worksheet = Model::new_empty_worksheet(sheet_name, sheet_id);
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(())
}
pub fn add_sheet(&mut self, sheet_name: &str) -> Result<(), String> {
self.insert_sheet(sheet_name, self.workbook.worksheets.len() as u32, None)
}
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))
}
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());
}
self.parser.set_lexer_mode(LexerMode::R1C1);
let worksheets = &mut self.workbook.worksheets;
for worksheet in worksheets {
let cell_reference = &Some(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;
}
self.parser.set_lexer_mode(LexerMode::A1);
let worksheets = &mut self.workbook.worksheets;
worksheets[sheet_index as usize].set_name(new_name);
self.reset_parsed_structures();
Ok(())
}
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(())
}
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())
}
}
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<u32> {
let worksheets = &self.workbook.worksheets;
for (index, worksheet) in worksheets.iter().enumerate() {
if worksheet.sheet_id == sheet_id {
return Some(index as u32);
}
}
None
}
pub fn new_empty(name: &str, locale_id: &str, timezone: &str) -> Result<Model, String> {
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 NaiveDateTime::from_timestamp_opt(seconds, 0) {
Some(s) => s,
None => return Err(format!("Invalid timestamp: {}", milliseconds)),
};
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
let workbook = Workbook {
shared_strings: vec![],
defined_names: vec![],
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1)],
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(),
};
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, HashMap::new());
let cells = HashMap::new();
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,
};
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")); assert!(!is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDE")); }
}