The context was optional because I thought that paring an RC formula did not need context. You at least need the sheet in which you are parsing For instance toknow if a defined name is local
439 lines
15 KiB
Rust
439 lines
15 KiB
Rust
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<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 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<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
|
|
}
|
|
|
|
/// Creates a new workbook with one empty sheet
|
|
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 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
|
|
}
|
|
}
|