Compare commits

..

1 Commits

Author SHA1 Message Date
Nicolás Hatcher
6db8c6228e BOB 2024-12-16 18:05:51 +01:00
58 changed files with 931 additions and 2717 deletions

View File

@@ -17,9 +17,8 @@ jobs:
- name: Generate code coverage - name: Generate code coverage
run: cargo llvm-cov --all-features --workspace --exclude pyroncalc --exclude wasm --lcov --output-path lcov.info run: cargo llvm-cov --all-features --workspace --exclude pyroncalc --exclude wasm --lcov --output-path lcov.info
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with: with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info files: lcov.info
fail_ci_if_error: true fail_ci_if_error: true

View File

@@ -2,7 +2,6 @@ use std::cmp::Ordering;
use crate::expressions::{token::Error, types::CellReferenceIndex}; use crate::expressions::{token::Error, types::CellReferenceIndex};
#[derive(Clone)]
pub struct Range { pub struct Range {
pub left: CellReferenceIndex, pub left: CellReferenceIndex,
pub right: CellReferenceIndex, pub right: CellReferenceIndex,

View File

@@ -164,9 +164,7 @@ pub enum Node {
args: Vec<Node>, args: Vec<Node>,
}, },
ArrayKind(Vec<Node>), ArrayKind(Vec<Node>),
DefinedNameKind((String, Option<u32>)), VariableKind(String),
TableNameKind(String),
WrongVariableKind(String),
CompareKind { CompareKind {
kind: OpCompare, kind: OpCompare,
left: Box<Node>, left: Box<Node>,
@@ -189,17 +187,12 @@ pub enum Node {
pub struct Parser { pub struct Parser {
lexer: lexer::Lexer, lexer: lexer::Lexer,
worksheets: Vec<String>, worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>, context: Option<CellReferenceRC>,
context: CellReferenceRC,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
} }
impl Parser { impl Parser {
pub fn new( pub fn new(worksheets: Vec<String>, tables: HashMap<String, Table>) -> Parser {
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
tables: HashMap<String, Table>,
) -> Parser {
let lexer = lexer::Lexer::new( let lexer = lexer::Lexer::new(
"", "",
lexer::LexerMode::A1, lexer::LexerMode::A1,
@@ -208,16 +201,10 @@ impl Parser {
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
get_language("en").expect(""), get_language("en").expect(""),
); );
let context = CellReferenceRC {
sheet: worksheets.first().map_or("", |v| v).to_string(),
column: 1,
row: 1,
};
Parser { Parser {
lexer, lexer,
worksheets, worksheets,
defined_names, context: None,
context,
tables, tables,
} }
} }
@@ -225,18 +212,13 @@ impl Parser {
self.lexer.set_lexer_mode(mode) self.lexer.set_lexer_mode(mode)
} }
pub fn set_worksheets_and_names( pub fn set_worksheets(&mut self, worksheets: Vec<String>) {
&mut self,
worksheets: Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
) {
self.worksheets = worksheets; self.worksheets = worksheets;
self.defined_names = defined_names;
} }
pub fn parse(&mut self, formula: &str, context: &CellReferenceRC) -> Node { pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
self.lexer.set_formula(formula); self.lexer.set_formula(formula);
self.context = context.clone(); self.context.clone_from(context);
self.parse_expr() self.parse_expr()
} }
@@ -250,24 +232,6 @@ impl Parser {
None None
} }
// Returns:
// * None: If there is no defined name by that name
// * Some(Some(index)): If there is a defined name local to that sheet
// * Some(None): If there is a global defined name
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<Option<u32>> {
for (df_name, df_scope) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
return Some(*df_scope);
}
}
for (df_name, df_scope) in &self.defined_names {
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
return Some(None);
}
}
None
}
fn parse_expr(&mut self) -> Node { fn parse_expr(&mut self) -> Node {
let mut t = self.parse_concat(); let mut t = self.parse_concat();
if let Node::ParseErrorKind { .. } = t { if let Node::ParseErrorKind { .. } = t {
@@ -482,7 +446,16 @@ impl Parser {
absolute_column, absolute_column,
absolute_row, absolute_row,
} => { } => {
let context = &self.context; let context = match &self.context {
Some(c) => c,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Expected context for the reference".to_string(),
}
}
};
let sheet_index = match &sheet { let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name), Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet), None => self.get_sheet_index_by_name(&context.sheet),
@@ -517,7 +490,16 @@ impl Parser {
} }
} }
TokenType::Range { sheet, left, right } => { TokenType::Range { sheet, left, right } => {
let context = &self.context; let context = match &self.context {
Some(c) => c,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: "Expected context for the reference".to_string(),
}
}
};
let sheet_index = match &sheet { let sheet_index = match &sheet {
Some(name) => self.get_sheet_index_by_name(name), Some(name) => self.get_sheet_index_by_name(name),
None => self.get_sheet_index_by_name(&context.sheet), None => self.get_sheet_index_by_name(&context.sheet),
@@ -603,33 +585,11 @@ impl Parser {
kind: function_kind, kind: function_kind,
args, args,
}; };
} } else {
return Node::InvalidFunctionKind { name, args }; return Node::InvalidFunctionKind { name, args };
}
let context = &self.context;
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
};
}
};
// Could be a defined name or a table
if let Some(scope) = self.get_defined_name(&name, context_sheet_index) {
return Node::DefinedNameKind((name, scope));
}
let name_lower = name.to_lowercase();
for table_name in self.tables.keys() {
if table_name.to_lowercase() == name_lower {
return Node::TableNameKind(name);
} }
} }
Node::WrongVariableKind(name) Node::VariableKind(name)
} }
TokenType::Error(kind) => Node::ErrorKind(kind), TokenType::Error(kind) => Node::ErrorKind(kind),
TokenType::Illegal(error) => Node::ParseErrorKind { TokenType::Illegal(error) => Node::ParseErrorKind {
@@ -701,177 +661,187 @@ impl Parser {
// We will try to convert to a normal reference // We will try to convert to a normal reference
// table_name[column_name] => cell1:cell2 // table_name[column_name] => cell1:cell2
// table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2 // table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2
let context = &self.context; if let Some(context) = &self.context {
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) { let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
Some(i) => i, Some(i) => i,
None => { None => {
return Node::ParseErrorKind { return Node::ParseErrorKind {
formula: self.lexer.get_formula(), formula: self.lexer.get_formula(),
position: 0, position: 0,
message: "sheet not found".to_string(), message: "sheet not found".to_string(),
};
}
};
// table-name => table
let table = match self.tables.get(&table_name) {
Some(t) => t,
None => {
let message = format!(
"Table not found: '{table_name}' at '{}!{}{}'",
context.sheet,
number_to_column(context.column)
.unwrap_or(format!("{}", context.column)),
context.row
);
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message,
};
}
};
let table_sheet_index = match self.get_sheet_index_by_name(&table.sheet_name) {
Some(i) => i,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "sheet not found".to_string(),
};
}
};
let sheet_name = if table_sheet_index == context_sheet_index {
None
} else {
Some(table.sheet_name.clone())
};
// context must be with tables.reference
#[allow(clippy::expect_used)]
let (column_start, mut row_start, column_end, mut row_end) =
parse_range(&table.reference).expect("Failed parsing range");
let totals_row_count = table.totals_row_count as i32;
let header_row_count = table.header_row_count as i32;
row_end -= totals_row_count;
match specifier {
Some(token::TableSpecifier::ThisRow) => {
row_start = context.row;
row_end = context.row;
}
Some(token::TableSpecifier::Totals) => {
if totals_row_count != 0 {
row_start = row_end + 1;
row_end = row_start;
} else {
// Table1[#Totals] is #REF! if Table1 does not have totals
return Node::ErrorKind(token::Error::REF);
}
}
Some(token::TableSpecifier::Headers) => {
row_end = row_start;
}
Some(token::TableSpecifier::Data) => {
row_start += header_row_count;
}
Some(token::TableSpecifier::All) => {
if totals_row_count != 0 {
row_end += 1;
}
}
None => {
// skip the headers
row_start += header_row_count;
}
}
match table_reference {
None => Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: column_start,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: column_end,
},
Some(TableReference::ColumnReference(s)) => {
let column_index = match get_table_column_by_name(&s, table) {
Some(s) => s + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!("Expecting column: {s} in table {table_name}"),
};
}
};
if row_start == row_end {
return Node::ReferenceKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row: true,
absolute_column: true,
row: row_start,
column: column_index,
}; };
} }
Node::RangeKind { };
sheet_name, // table-name => table
sheet_index: table_sheet_index, let table = match self.tables.get(&table_name) {
absolute_row1: true, Some(t) => t,
absolute_column1: true, None => {
row1: row_start, let message = format!(
column1: column_index, "Table not found: '{table_name}' at '{}!{}{}'",
absolute_row2: true, context.sheet,
absolute_column2: true, number_to_column(context.column)
row2: row_end, .unwrap_or(format!("{}", context.column)),
column2: column_index, context.row
);
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message,
};
} }
} };
Some(TableReference::RangeReference((left, right))) => { let table_sheet_index = match self.get_sheet_index_by_name(&table.sheet_name) {
let left_column_index = match get_table_column_by_name(&left, table) { Some(i) => i,
Some(f) => f + column_start, None => {
None => { return Node::ParseErrorKind {
return Node::ParseErrorKind { formula: self.lexer.get_formula(),
formula: self.lexer.get_formula(), position: 0,
position: self.lexer.get_position() as usize, message: "sheet not found".to_string(),
message: format!( };
"Expecting column: {left} in table {table_name}" }
), };
};
}
};
let right_column_index = match get_table_column_by_name(&right, table) { let sheet_name = if table_sheet_index == context_sheet_index {
Some(f) => f + column_start, None
None => { } else {
return Node::ParseErrorKind { Some(table.sheet_name.clone())
formula: self.lexer.get_formula(), };
position: self.lexer.get_position() as usize,
message: format!( // context must be with tables.reference
"Expecting column: {right} in table {table_name}" #[allow(clippy::expect_used)]
), let (column_start, mut row_start, column_end, mut row_end) =
}; parse_range(&table.reference).expect("Failed parsing range");
let totals_row_count = table.totals_row_count as i32;
let header_row_count = table.header_row_count as i32;
row_end -= totals_row_count;
match specifier {
Some(token::TableSpecifier::ThisRow) => {
row_start = context.row;
row_end = context.row;
}
Some(token::TableSpecifier::Totals) => {
if totals_row_count != 0 {
row_start = row_end + 1;
row_end = row_start;
} else {
// Table1[#Totals] is #REF! if Table1 does not have totals
return Node::ErrorKind(token::Error::REF);
} }
}; }
Node::RangeKind { Some(token::TableSpecifier::Headers) => {
sheet_name, row_end = row_start;
sheet_index: table_sheet_index, }
absolute_row1: true, Some(token::TableSpecifier::Data) => {
absolute_column1: true, row_start += header_row_count;
row1: row_start, }
column1: left_column_index, Some(token::TableSpecifier::All) => {
absolute_row2: true, if totals_row_count != 0 {
absolute_column2: true, row_end += 1;
row2: row_end, }
column2: right_column_index, }
None => {
// skip the headers
row_start += header_row_count;
} }
} }
match table_reference {
None => {
return Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: column_start,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: column_end,
};
}
Some(TableReference::ColumnReference(s)) => {
let column_index = match get_table_column_by_name(&s, table) {
Some(s) => s + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!(
"Expecting column: {s} in table {table_name}"
),
};
}
};
if row_start == row_end {
return Node::ReferenceKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row: true,
absolute_column: true,
row: row_start,
column: column_index,
};
}
return Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: column_index,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: column_index,
};
}
Some(TableReference::RangeReference((left, right))) => {
let left_column_index = match get_table_column_by_name(&left, table) {
Some(f) => f + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!(
"Expecting column: {left} in table {table_name}"
),
};
}
};
let right_column_index = match get_table_column_by_name(&right, table) {
Some(f) => f + column_start,
None => {
return Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: self.lexer.get_position() as usize,
message: format!(
"Expecting column: {right} in table {table_name}"
),
};
}
};
return Node::RangeKind {
sheet_name,
sheet_index: table_sheet_index,
absolute_row1: true,
absolute_column1: true,
row1: row_start,
column1: left_column_index,
absolute_row2: true,
absolute_column2: true,
row2: row_end,
column2: right_column_index,
};
}
}
}
Node::ParseErrorKind {
formula: self.lexer.get_formula(),
position: 0,
message: "Structured references not supported in R1C1 mode".to_string(),
} }
} }
} }

View File

@@ -375,9 +375,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
} }
format!("{{{}}}", arguments) format!("{{{}}}", arguments)
} }
DefinedNameKind((name, _)) => name.to_string(), VariableKind(value) => value.to_string(),
TableNameKind(name) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
CompareKind { kind, left, right } => format!( CompareKind { kind, left, right } => format!(
"{}{}{}", "{}{}{}",
to_string_moved(left, move_context), to_string_moved(left, move_context),

View File

@@ -464,9 +464,7 @@ fn stringify(
| ReferenceKind { .. } | ReferenceKind { .. }
| RangeKind { .. } | RangeKind { .. }
| WrongReferenceKind { .. } | WrongReferenceKind { .. }
| DefinedNameKind(_) | VariableKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => { | WrongRangeKind { .. } => {
stringify(left, context, displace_data, use_original_name) stringify(left, context, displace_data, use_original_name)
} }
@@ -494,9 +492,7 @@ fn stringify(
| ReferenceKind { .. } | ReferenceKind { .. }
| RangeKind { .. } | RangeKind { .. }
| WrongReferenceKind { .. } | WrongReferenceKind { .. }
| DefinedNameKind(_) | VariableKind(_)
| TableNameKind(_)
| WrongVariableKind(_)
| WrongRangeKind { .. } => { | WrongRangeKind { .. } => {
stringify(right, context, displace_data, use_original_name) stringify(right, context, displace_data, use_original_name)
} }
@@ -547,9 +543,7 @@ fn stringify(
} }
format!("{{{}}}", arguments) format!("{{{}}}", arguments)
} }
TableNameKind(value) => value.to_string(), VariableKind(value) => value.to_string(),
DefinedNameKind((name, _)) => name.to_string(),
WrongVariableKind(name) => name.to_string(),
UnaryKind { kind, right } => match kind { UnaryKind { kind, right } => match kind {
OpUnary::Minus => { OpUnary::Minus => {
format!( format!(
@@ -666,90 +660,7 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
Node::ErrorKind(_) => {} Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
Node::ArrayKind(_) => {} Node::ArrayKind(_) => {}
Node::DefinedNameKind(_) => {} Node::VariableKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::EmptyArgKind => {} Node::EmptyArgKind => {}
} }
} }
pub(crate) fn rename_defined_name_in_node(
node: &mut Node,
name: &str,
scope: Option<u32>,
new_name: &str,
) {
match node {
// Rename
Node::DefinedNameKind((n, s)) => {
if name.to_lowercase() == n.to_lowercase() && *s == scope {
*n = new_name.to_string();
}
}
// Go next level
Node::OpRangeKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpConcatenateKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpSumKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpProductKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::OpPowerKind { left, right } => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::FunctionKind { kind: _, args } => {
for arg in args {
rename_defined_name_in_node(arg, name, scope, new_name);
}
}
Node::InvalidFunctionKind { name: _, args } => {
for arg in args {
rename_defined_name_in_node(arg, name, scope, new_name);
}
}
Node::CompareKind {
kind: _,
left,
right,
} => {
rename_defined_name_in_node(left, name, scope, new_name);
rename_defined_name_in_node(right, name, scope, new_name);
}
Node::UnaryKind { kind: _, right } => {
rename_defined_name_in_node(right, name, scope, new_name);
}
// Do nothing
Node::BooleanKind(_) => {}
Node::NumberKind(_) => {}
Node::StringKind(_) => {}
Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {}
Node::ArrayKind(_) => {}
Node::EmptyArgKind => {}
Node::ReferenceKind { .. } => {}
Node::RangeKind { .. } => {}
Node::WrongReferenceKind { .. } => {}
Node::WrongRangeKind { .. } => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
}
}

View File

@@ -17,7 +17,7 @@ struct Formula<'a> {
#[test] #[test]
fn test_parser_reference() { fn test_parser_reference() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -25,14 +25,14 @@ fn test_parser_reference() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("A2", &cell_reference); let t = parser.parse("A2", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[1]C[0]"); assert_eq!(to_rc_format(&t), "R[1]C[0]");
} }
#[test] #[test]
fn test_parser_absolute_column() { fn test_parser_absolute_column() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -40,14 +40,14 @@ fn test_parser_absolute_column() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$A1", &cell_reference); let t = parser.parse("$A1", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[0]C1"); assert_eq!(to_rc_format(&t), "R[0]C1");
} }
#[test] #[test]
fn test_parser_absolute_row_col() { fn test_parser_absolute_row_col() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -55,14 +55,14 @@ fn test_parser_absolute_row_col() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$C$5", &cell_reference); let t = parser.parse("$C$5", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R5C3"); assert_eq!(to_rc_format(&t), "R5C3");
} }
#[test] #[test]
fn test_parser_absolute_row_col_1() { fn test_parser_absolute_row_col_1() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -70,14 +70,14 @@ fn test_parser_absolute_row_col_1() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("$A$1", &cell_reference); let t = parser.parse("$A$1", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R1C1"); assert_eq!(to_rc_format(&t), "R1C1");
} }
#[test] #[test]
fn test_parser_simple_formula() { fn test_parser_simple_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -86,14 +86,14 @@ fn test_parser_simple_formula() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+Sheet2!D4", &cell_reference); let t = parser.parse("C3+Sheet2!D4", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_boolean() { fn test_parser_boolean() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -102,14 +102,14 @@ fn test_parser_boolean() {
column: 1, column: 1,
}; };
let t = parser.parse("true", &cell_reference); let t = parser.parse("true", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "TRUE"); assert_eq!(to_rc_format(&t), "TRUE");
} }
#[test] #[test]
fn test_parser_bad_formula() { fn test_parser_bad_formula() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -117,7 +117,7 @@ fn test_parser_bad_formula() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("#Value", &cell_reference); let t = parser.parse("#Value", &Some(cell_reference));
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -138,7 +138,7 @@ fn test_parser_bad_formula() {
#[test] #[test]
fn test_parser_bad_formula_1() { fn test_parser_bad_formula_1() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -146,7 +146,7 @@ fn test_parser_bad_formula_1() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("<5", &cell_reference); let t = parser.parse("<5", &Some(cell_reference));
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -167,7 +167,7 @@ fn test_parser_bad_formula_1() {
#[test] #[test]
fn test_parser_bad_formula_2() { fn test_parser_bad_formula_2() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -175,7 +175,7 @@ fn test_parser_bad_formula_2() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("*5", &cell_reference); let t = parser.parse("*5", &Some(cell_reference));
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -196,7 +196,7 @@ fn test_parser_bad_formula_2() {
#[test] #[test]
fn test_parser_bad_formula_3() { fn test_parser_bad_formula_3() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -204,7 +204,7 @@ fn test_parser_bad_formula_3() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("SUM(#VALVE!)", &cell_reference); let t = parser.parse("SUM(#VALVE!)", &Some(cell_reference));
match &t { match &t {
Node::ParseErrorKind { Node::ParseErrorKind {
formula, formula,
@@ -225,7 +225,7 @@ fn test_parser_bad_formula_3() {
#[test] #[test]
fn test_parser_formulas() { fn test_parser_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let formulas = vec![ let formulas = vec![
Formula { Formula {
@@ -259,11 +259,11 @@ fn test_parser_formulas() {
for formula in formulas { for formula in formulas {
let t = parser.parse( let t = parser.parse(
formula.initial, formula.initial,
&CellReferenceRC { &Some(CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}, }),
); );
assert_eq!(to_rc_format(&t), formula.expected); assert_eq!(to_rc_format(&t), formula.expected);
assert_eq!(to_string(&t, &cell_reference), formula.initial); assert_eq!(to_string(&t, &cell_reference), formula.initial);
@@ -273,7 +273,7 @@ fn test_parser_formulas() {
#[test] #[test]
fn test_parser_r1c1_formulas() { fn test_parser_r1c1_formulas() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
parser.set_lexer_mode(LexerMode::R1C1); parser.set_lexer_mode(LexerMode::R1C1);
let formulas = vec![ let formulas = vec![
@@ -324,11 +324,11 @@ fn test_parser_r1c1_formulas() {
for formula in formulas { for formula in formulas {
let t = parser.parse( let t = parser.parse(
formula.initial, formula.initial,
&CellReferenceRC { &Some(CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}, }),
); );
assert_eq!(to_string(&t, &cell_reference), formula.expected); assert_eq!(to_string(&t, &cell_reference), formula.expected);
assert_eq!(to_rc_format(&t), formula.initial); assert_eq!(to_rc_format(&t), formula.initial);
@@ -338,7 +338,7 @@ fn test_parser_r1c1_formulas() {
#[test] #[test]
fn test_parser_quotes() { fn test_parser_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -347,14 +347,14 @@ fn test_parser_quotes() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+'Second Sheet'!D4", &cell_reference); let t = parser.parse("C3+'Second Sheet'!D4", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_escape_quotes() { fn test_parser_escape_quotes() {
let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -363,14 +363,14 @@ fn test_parser_escape_quotes() {
column: 1, column: 1,
}; };
let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &cell_reference); let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]"); assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]");
} }
#[test] #[test]
fn test_parser_parenthesis() { fn test_parser_parenthesis() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -379,14 +379,14 @@ fn test_parser_parenthesis() {
column: 1, column: 1,
}; };
let t = parser.parse("(C3=\"Yes\")*5", &cell_reference); let t = parser.parse("(C3=\"Yes\")*5", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5"); assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5");
} }
#[test] #[test]
fn test_parser_excel_xlfn() { fn test_parser_excel_xlfn() {
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -395,7 +395,7 @@ fn test_parser_excel_xlfn() {
column: 1, column: 1,
}; };
let t = parser.parse("_xlfn.CONCAT(C3)", &cell_reference); let t = parser.parse("_xlfn.CONCAT(C3)", &Some(cell_reference));
assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])"); assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])");
} }
@@ -407,9 +407,9 @@ fn test_to_string_displaced() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let node = parser.parse("C3", context); let node = parser.parse("C3", &Some(context.clone()));
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -427,9 +427,9 @@ fn test_to_string_displaced_full_ranges() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let node = parser.parse("SUM(3:3)", context); let node = parser.parse("SUM(3:3)", &Some(context.clone()));
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -440,7 +440,7 @@ fn test_to_string_displaced_full_ranges() {
"SUM(3:3)".to_string() "SUM(3:3)".to_string()
); );
let node = parser.parse("SUM(D:D)", context); let node = parser.parse("SUM(D:D)", &Some(context.clone()));
let displace_data = DisplaceData::Row { let displace_data = DisplaceData::Row {
sheet: 0, sheet: 0,
row: 3, row: 3,
@@ -460,9 +460,9 @@ fn test_to_string_displaced_too_low() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let node = parser.parse("C3", context); let node = parser.parse("C3", &Some(context.clone()));
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,
@@ -480,9 +480,9 @@ fn test_to_string_displaced_too_high() {
column: 1, column: 1,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let node = parser.parse("C3", context); let node = parser.parse("C3", &Some(context.clone()));
let displace_data = DisplaceData::Column { let displace_data = DisplaceData::Column {
sheet: 0, sheet: 0,
column: 1, column: 1,

View File

@@ -9,7 +9,7 @@ use crate::expressions::types::CellReferenceRC;
#[test] #[test]
fn issue_155_parser() { fn issue_155_parser() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -17,14 +17,14 @@ fn issue_155_parser() {
row: 2, row: 2,
column: 2, column: 2,
}; };
let t = parser.parse("A$1:A2", &cell_reference); let t = parser.parse("A$1:A2", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "A$1:A2"); assert_eq!(to_string(&t, &cell_reference), "A$1:A2");
} }
#[test] #[test]
fn issue_155_parser_case_2() { fn issue_155_parser_case_2() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -32,14 +32,14 @@ fn issue_155_parser_case_2() {
row: 20, row: 20,
column: 20, column: 20,
}; };
let t = parser.parse("C$1:D2", &cell_reference); let t = parser.parse("C$1:D2", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "C$1:D2"); assert_eq!(to_string(&t, &cell_reference), "C$1:D2");
} }
#[test] #[test]
fn issue_155_parser_only_row() { fn issue_155_parser_only_row() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -48,14 +48,14 @@ fn issue_155_parser_only_row() {
column: 20, column: 20,
}; };
// This is tricky, I am not sure what to do in these cases // This is tricky, I am not sure what to do in these cases
let t = parser.parse("A$2:B1", &cell_reference); let t = parser.parse("A$2:B1", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "A1:B$2"); assert_eq!(to_string(&t, &cell_reference), "A1:B$2");
} }
#[test] #[test]
fn issue_155_parser_only_column() { fn issue_155_parser_only_column() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -64,6 +64,6 @@ fn issue_155_parser_only_column() {
column: 20, column: 20,
}; };
// This is tricky, I am not sure what to do in these cases // This is tricky, I am not sure what to do in these cases
let t = parser.parse("D1:$A3", &cell_reference); let t = parser.parse("D1:$A3", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "$A1:D3"); assert_eq!(to_string(&t, &cell_reference), "$A1:D3");
} }

View File

@@ -15,7 +15,7 @@ fn test_move_formula() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -27,7 +27,7 @@ fn test_move_formula() {
}; };
// formula AB31 will not change // formula AB31 will not change
let node = parser.parse("AB31", context); let node = parser.parse("AB31", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -43,7 +43,7 @@ fn test_move_formula() {
assert_eq!(t, "AB31"); assert_eq!(t, "AB31");
// formula $AB$31 will not change // formula $AB$31 will not change
let node = parser.parse("AB31", context); let node = parser.parse("AB31", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -59,7 +59,7 @@ fn test_move_formula() {
assert_eq!(t, "AB31"); assert_eq!(t, "AB31");
// but formula D5 will change to N15 (N = D + 10) // but formula D5 will change to N15 (N = D + 10)
let node = parser.parse("D5", context); let node = parser.parse("D5", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -75,7 +75,7 @@ fn test_move_formula() {
assert_eq!(t, "N15"); assert_eq!(t, "N15");
// Also formula $D$5 will change to N15 (N = D + 10) // Also formula $D$5 will change to N15 (N = D + 10)
let node = parser.parse("$D$5", context); let node = parser.parse("$D$5", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -102,7 +102,7 @@ fn test_move_formula_context_offset() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -113,7 +113,7 @@ fn test_move_formula_context_offset() {
height: 5, height: 5,
}; };
let node = parser.parse("-X9+C2%", context); let node = parser.parse("-X9+C2%", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -140,7 +140,7 @@ fn test_move_formula_area_limits() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -152,7 +152,7 @@ fn test_move_formula_area_limits() {
}; };
// Outside of the area. Not moved // Outside of the area. Not moved
let node = parser.parse("B2+B3+C1+G6+H5", context); let node = parser.parse("B2+B3+C1+G6+H5", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -168,7 +168,7 @@ fn test_move_formula_area_limits() {
assert_eq!(t, "B2+B3+C1+G6+H5"); assert_eq!(t, "B2+B3+C1+G6+H5");
// In the area. Moved // In the area. Moved
let node = parser.parse("C2+F4+F5+F6", context); let node = parser.parse("C2+F4+F5+F6", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -195,7 +195,7 @@ fn test_move_formula_ranges() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let area = &Area { let area = &Area {
sheet: 0, sheet: 0,
@@ -205,7 +205,7 @@ fn test_move_formula_ranges() {
height: 5, height: 5,
}; };
// Ranges inside the area are fully displaced (absolute or not) // Ranges inside the area are fully displaced (absolute or not)
let node = parser.parse("SUM(C2:F5)", context); let node = parser.parse("SUM(C2:F5)", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -220,7 +220,7 @@ fn test_move_formula_ranges() {
); );
assert_eq!(t, "SUM(M12:P15)"); assert_eq!(t, "SUM(M12:P15)");
let node = parser.parse("SUM($C$2:$F$5)", context); let node = parser.parse("SUM($C$2:$F$5)", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -236,7 +236,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM($M$12:$P$15)"); assert_eq!(t, "SUM($M$12:$P$15)");
// Ranges completely outside of the area are not touched // Ranges completely outside of the area are not touched
let node = parser.parse("SUM(A1:B3)", context); let node = parser.parse("SUM(A1:B3)", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -251,7 +251,7 @@ fn test_move_formula_ranges() {
); );
assert_eq!(t, "SUM(A1:B3)"); assert_eq!(t, "SUM(A1:B3)");
let node = parser.parse("SUM($A$1:$B$3)", context); let node = parser.parse("SUM($A$1:$B$3)", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -267,7 +267,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM($A$1:$B$3)"); assert_eq!(t, "SUM($A$1:$B$3)");
// Ranges that overlap with the area are also NOT displaced // Ranges that overlap with the area are also NOT displaced
let node = parser.parse("SUM(A1:F5)", context); let node = parser.parse("SUM(A1:F5)", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -283,7 +283,7 @@ fn test_move_formula_ranges() {
assert_eq!(t, "SUM(A1:F5)"); assert_eq!(t, "SUM(A1:F5)");
// Ranges that contain the area are also NOT displaced // Ranges that contain the area are also NOT displaced
let node = parser.parse("SUM(A1:X50)", context); let node = parser.parse("SUM(A1:X50)", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -318,10 +318,10 @@ fn test_move_formula_wrong_reference() {
height: 5, height: 5,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Wrong formulas will NOT be displaced // Wrong formulas will NOT be displaced
let node = parser.parse("Sheet3!AB31", context); let node = parser.parse("Sheet3!AB31", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -335,7 +335,7 @@ fn test_move_formula_wrong_reference() {
}, },
); );
assert_eq!(t, "Sheet3!AB31"); assert_eq!(t, "Sheet3!AB31");
let node = parser.parse("Sheet3!$X$9", context); let node = parser.parse("Sheet3!$X$9", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -350,7 +350,7 @@ fn test_move_formula_wrong_reference() {
); );
assert_eq!(t, "Sheet3!$X$9"); assert_eq!(t, "Sheet3!$X$9");
let node = parser.parse("SUM(Sheet3!D2:D3)", context); let node = parser.parse("SUM(Sheet3!D2:D3)", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -377,7 +377,7 @@ fn test_move_formula_misc() {
column, column,
}; };
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -387,7 +387,7 @@ fn test_move_formula_misc() {
width: 4, width: 4,
height: 5, height: 5,
}; };
let node = parser.parse("X9^C2-F4*H2", context); let node = parser.parse("X9^C2-F4*H2", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -402,7 +402,7 @@ fn test_move_formula_misc() {
); );
assert_eq!(t, "X9^M12-P14*H2"); assert_eq!(t, "X9^M12-P14*H2");
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context); let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -417,7 +417,7 @@ fn test_move_formula_misc() {
); );
assert_eq!(t, "P15*(-N15)*SUM(A1,X9,$N$15)"); assert_eq!(t, "P15*(-N15)*SUM(A1,X9,$N$15)");
let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", context); let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", &Some(context.clone()));
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {
@@ -445,7 +445,7 @@ fn test_move_formula_another_sheet() {
}; };
// we add two sheets and we cut/paste from Sheet1 to Sheet2 // we add two sheets and we cut/paste from Sheet1 to Sheet2
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Area is C2:F6 // Area is C2:F6
let area = &Area { let area = &Area {
@@ -457,7 +457,10 @@ fn test_move_formula_another_sheet() {
}; };
// Formula AB31 and JJ3:JJ4 refers to original Sheet1!AB31 and Sheet1!JJ3:JJ4 // Formula AB31 and JJ3:JJ4 refers to original Sheet1!AB31 and Sheet1!JJ3:JJ4
let node = parser.parse("AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)", context); let node = parser.parse(
"AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)",
&Some(context.clone()),
);
let t = move_formula( let t = move_formula(
&node, &node,
&MoveContext { &MoveContext {

View File

@@ -14,7 +14,7 @@ struct Formula<'a> {
#[test] #[test]
fn test_parser_formulas_with_full_ranges() { fn test_parser_formulas_with_full_ranges() {
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
let formulas = vec![ let formulas = vec![
Formula { Formula {
@@ -52,11 +52,11 @@ fn test_parser_formulas_with_full_ranges() {
for formula in &formulas { for formula in &formulas {
let t = parser.parse( let t = parser.parse(
formula.formula_a1, formula.formula_a1,
&CellReferenceRC { &Some(CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}, }),
); );
assert_eq!(to_rc_format(&t), formula.formula_r1c1); assert_eq!(to_rc_format(&t), formula.formula_r1c1);
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1); assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
@@ -67,11 +67,11 @@ fn test_parser_formulas_with_full_ranges() {
for formula in &formulas { for formula in &formulas {
let t = parser.parse( let t = parser.parse(
formula.formula_r1c1, formula.formula_r1c1,
&CellReferenceRC { &Some(CellReferenceRC {
sheet: "Sheet1".to_string(), sheet: "Sheet1".to_string(),
row: 1, row: 1,
column: 1, column: 1,
}, }),
); );
assert_eq!(to_rc_format(&t), formula.formula_r1c1); assert_eq!(to_rc_format(&t), formula.formula_r1c1);
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1); assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
@@ -81,7 +81,7 @@ fn test_parser_formulas_with_full_ranges() {
#[test] #[test]
fn test_range_inverse_order() { fn test_range_inverse_order() {
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()]; let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -93,7 +93,7 @@ fn test_range_inverse_order() {
// D4:C2 => C2:D4 // D4:C2 => C2:D4
let t = parser.parse( let t = parser.parse(
"SUM(D4:C2)*SUM(Sheet2!D4:C20)*SUM($C$20:D4)", "SUM(D4:C2)*SUM(Sheet2!D4:C20)*SUM($C$20:D4)",
&cell_reference, &Some(cell_reference.clone()),
); );
assert_eq!( assert_eq!(
to_string(&t, &cell_reference), to_string(&t, &cell_reference),

View File

@@ -9,7 +9,7 @@ use crate::expressions::types::CellReferenceRC;
#[test] #[test]
fn exp_order() { fn exp_order() {
let worksheets = vec!["Sheet1".to_string()]; let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new()); let mut parser = Parser::new(worksheets, HashMap::new());
// Reference cell is Sheet1!A1 // Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
@@ -17,18 +17,18 @@ fn exp_order() {
row: 1, row: 1,
column: 1, column: 1,
}; };
let t = parser.parse("(1 + 2)^3 + 4", &cell_reference); let t = parser.parse("(1 + 2)^3 + 4", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "(1+2)^3+4"); assert_eq!(to_string(&t, &cell_reference), "(1+2)^3+4");
let t = parser.parse("(C5 + 3)^R4", &cell_reference); let t = parser.parse("(C5 + 3)^R4", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^R4"); assert_eq!(to_string(&t, &cell_reference), "(C5+3)^R4");
let t = parser.parse("(C5 + 3)^(R4*6)", &cell_reference); let t = parser.parse("(C5 + 3)^(R4*6)", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "(C5+3)^(R4*6)"); assert_eq!(to_string(&t, &cell_reference), "(C5+3)^(R4*6)");
let t = parser.parse("(C5)^(R4)", &cell_reference); let t = parser.parse("(C5)^(R4)", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "C5^R4"); assert_eq!(to_string(&t, &cell_reference), "C5^R4");
let t = parser.parse("(5)^(4)", &cell_reference); let t = parser.parse("(5)^(4)", &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "5^4"); assert_eq!(to_string(&t, &cell_reference), "5^4");
} }

View File

@@ -63,7 +63,7 @@ fn simple_table() {
let row_count = 3; let row_count = 3;
let tables = create_test_table("tblIncome", &column_names, "A1", row_count); let tables = create_test_table("tblIncome", &column_names, "A1", row_count);
let mut parser = Parser::new(worksheets, vec![], tables); let mut parser = Parser::new(worksheets, tables);
// Reference cell is 'Sheet One'!F2 // Reference cell is 'Sheet One'!F2
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
sheet: "Sheet One".to_string(), sheet: "Sheet One".to_string(),
@@ -72,7 +72,7 @@ fn simple_table() {
}; };
let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])"; let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])";
let t = parser.parse(formula, &cell_reference); let t = parser.parse(formula, &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)"); assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)");
// Cell A3 // Cell A3
@@ -82,7 +82,7 @@ fn simple_table() {
column: 1, column: 1,
}; };
let formula = "SUBTOTAL(109, tblIncome[Jan])"; let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &cell_reference); let t = parser.parse(formula, &Some(cell_reference.clone()));
assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)"); assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)");
// Cell A3 in 'Second Sheet' // Cell A3 in 'Second Sheet'
@@ -92,7 +92,7 @@ fn simple_table() {
column: 1, column: 1,
}; };
let formula = "SUBTOTAL(109, tblIncome[Jan])"; let formula = "SUBTOTAL(109, tblIncome[Jan])";
let t = parser.parse(formula, &cell_reference); let t = parser.parse(formula, &Some(cell_reference.clone()));
assert_eq!( assert_eq!(
to_string(&t, &cell_reference), to_string(&t, &cell_reference),
"SUBTOTAL(109,'Sheet One'!$A$2:$A$3)" "SUBTOTAL(109,'Sheet One'!$A$2:$A$3)"

View File

@@ -263,9 +263,7 @@ pub(crate) fn forward_references(
// TODO: Not implemented // TODO: Not implemented
Node::ArrayKind(_) => {} Node::ArrayKind(_) => {}
// Do nothing. Note: we could do a blanket _ => {} // Do nothing. Note: we could do a blanket _ => {}
Node::DefinedNameKind(_) => {} Node::VariableKind(_) => {}
Node::TableNameKind(_) => {}
Node::WrongVariableKind(_) => {}
Node::ErrorKind(_) => {} Node::ErrorKind(_) => {}
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
Node::EmptyArgKind => {} Node::EmptyArgKind => {}

View File

@@ -247,68 +247,46 @@ impl Model {
return CalcResult::Number(cell.sheet as f64 + 1.0); return CalcResult::Number(cell.sheet as f64 + 1.0);
} }
// The arg could be a defined name or a table // The arg could be a defined name or a table
// let = &args[0]; let arg = &args[0];
match &args[0] { if let Node::VariableKind(name) = arg {
Node::DefinedNameKind((name, scope)) => { // Let's see if it is a defined name
// Let's see if it is a defined name if let Some(defined_name) = self.parsed_defined_names.get(&(None, name.to_lowercase()))
if let Some(defined_name) = self {
.parsed_defined_names match defined_name {
.get(&(*scope, name.to_lowercase())) ParsedDefinedName::CellReference(reference) => {
{ return CalcResult::Number(reference.sheet as f64 + 1.0)
match defined_name {
ParsedDefinedName::CellReference(reference) => {
return CalcResult::Number(reference.sheet as f64 + 1.0)
}
ParsedDefinedName::RangeReference(range) => {
return CalcResult::Number(range.left.sheet as f64 + 1.0)
}
ParsedDefinedName::InvalidDefinedNameFormula => {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Invalid name".to_string(),
};
}
} }
} else { ParsedDefinedName::RangeReference(range) => {
// This should never happen return CalcResult::Number(range.left.sheet as f64 + 1.0)
return CalcResult::Error { }
error: Error::ERROR, ParsedDefinedName::InvalidDefinedNameFormula => {
origin: cell, return CalcResult::Error {
message: "Invalid name".to_string(), error: Error::NA,
}; origin: cell,
} message: "Invalid name".to_string(),
} };
Node::TableNameKind(name) => {
// Now let's see if it is a table
for (table_name, table) in &self.workbook.tables {
if table_name == name {
if let Some(sheet_index) = self.get_sheet_index_by_name(&table.sheet_name) {
return CalcResult::Number(sheet_index as f64 + 1.0);
} else {
break;
}
} }
} }
} }
Node::WrongVariableKind(name) => { // Now let's see if it is a table
return CalcResult::Error { for (table_name, table) in &self.workbook.tables {
error: Error::NAME, if table_name == name {
origin: cell, if let Some(sheet_index) = self.get_sheet_index_by_name(&table.sheet_name) {
message: format!("Name not found: {name}"), return CalcResult::Number(sheet_index as f64 + 1.0);
} } else {
} break;
arg => { }
// Now it should be the name of a sheet
let sheet_name = match self.get_string(arg, cell) {
Ok(s) => s,
Err(e) => return e,
};
if let Some(sheet_index) = self.get_sheet_index_by_name(&sheet_name) {
return CalcResult::Number(sheet_index as f64 + 1.0);
} }
} }
} }
// Now it should be the name of a sheet
let sheet_name = match self.get_string(arg, cell) {
Ok(s) => s,
Err(e) => return e,
};
if let Some(sheet_index) = self.get_sheet_index_by_name(&sheet_name) {
return CalcResult::Number(sheet_index as f64 + 1.0);
}
CalcResult::Error { CalcResult::Error {
error: Error::NA, error: Error::NA,
origin: cell, origin: cell,

View File

@@ -66,61 +66,24 @@ impl Model {
} }
pub(crate) fn fn_and(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_and(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.logical_nary( let mut true_count = 0;
args,
cell,
|acc, value| acc.unwrap_or(true) && value,
Some(false),
)
}
pub(crate) fn fn_or(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.logical_nary(
args,
cell,
|acc, value| acc.unwrap_or(false) || value,
Some(true),
)
}
pub(crate) fn fn_xor(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.logical_nary(args, cell, |acc, value| acc.unwrap_or(false) ^ value, None)
}
/// Base function for AND, OR, XOR. These are all n-ary functions that perform a boolean operation on a series of
/// boolean values. These boolean values are sourced from `args`. Note that there is not a 1-1 relationship between
/// arguments and boolean values evaluated (see how Ranges are handled for example).
///
/// Each argument in `args` is evaluated and the resulting value is interpreted as a boolean as follows:
/// - Boolean: The value is used directly.
/// - Number: 0 is FALSE, all other values are TRUE.
/// - Range: Each cell in the range is evaluated as if they were individual arguments with some caveats
/// - Empty arg: FALSE
/// - Empty cell & String: Ignored, behaves exactly like the argument wasn't passed in at all
/// - Error: Propagated
///
/// If no arguments are provided, or all arguments are ignored, the function returns a #VALUE! error
///
/// **`fold_fn`:** The function that combines the running result with the next value boolean value. The running result
/// starts as `None`.
///
/// **`short_circuit_value`:** If the running result reaches `short_circuit_value`, the function returns early.
fn logical_nary(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
fold_fn: fn(Option<bool>, bool) -> bool,
short_circuit_value: Option<bool>,
) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut result = None;
for arg in args { for arg in args {
match self.evaluate_node_in_context(arg, cell) { match self.evaluate_node_in_context(arg, cell) {
CalcResult::Boolean(value) => result = Some(fold_fn(result, value)), CalcResult::Boolean(b) => {
CalcResult::Number(value) => result = Some(fold_fn(result, value != 0.0)), if !b {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::Number(value) => {
if value == 0.0 {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::String(_value) => {
true_count += 1;
}
CalcResult::Range { left, right } => { CalcResult::Range { left, right } => {
if left.sheet != right.sheet { if left.sheet != right.sheet {
return CalcResult::new_error( return CalcResult::new_error(
@@ -136,58 +99,161 @@ impl Model {
row, row,
column, column,
}) { }) {
CalcResult::Boolean(value) => result = Some(fold_fn(result, value)), CalcResult::Boolean(b) => {
if !b {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::Number(value) => { CalcResult::Number(value) => {
result = Some(fold_fn(result, value != 0.0)) if value == 0.0 {
return CalcResult::Boolean(false);
}
true_count += 1;
}
CalcResult::String(_value) => {
true_count += 1;
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyArg => {} // unreachable CalcResult::Range { .. } => {}
CalcResult::Range { .. } CalcResult::EmptyCell | CalcResult::EmptyArg => {}
| CalcResult::String { .. }
| CalcResult::EmptyCell => {}
}
if let (Some(current_result), Some(short_circuit_value)) =
(result, short_circuit_value)
{
if current_result == short_circuit_value {
return CalcResult::Boolean(current_result);
}
} }
} }
} }
} }
error @ CalcResult::Error { .. } => return error, error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyArg => result = Some(result.unwrap_or(false)), CalcResult::EmptyCell | CalcResult::EmptyArg => {}
// Strings are ignored unless they are "TRUE" or "FALSE" (case insensitive). EXCEPT if the string value };
// comes from a reference, in which case it is always ignored regardless of its value. }
CalcResult::String(..) => { if true_count == 0 {
if !matches!(arg, Node::ReferenceKind { .. }) { return CalcResult::new_error(
if let Ok(f) = self.get_boolean(arg, cell) { Error::VALUE,
result = Some(fold_fn(result, f)); cell,
"Boolean values not found".to_string(),
);
}
CalcResult::Boolean(true)
}
pub(crate) fn fn_or(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut result = false;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Boolean(value) => result = value || result,
CalcResult::Number(value) => {
if value != 0.0 {
return CalcResult::Boolean(true);
}
}
CalcResult::String(_value) => {
return CalcResult::Boolean(true);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Boolean(value) => {
result = value || result;
}
CalcResult::Number(value) => {
if value != 0.0 {
return CalcResult::Boolean(true);
}
}
CalcResult::String(_value) => {
return CalcResult::Boolean(true);
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {}
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
}
} }
} }
} }
// References to empty cells are ignored. If all args are ignored the result is #VALUE! error @ CalcResult::Error { .. } => return error,
CalcResult::EmptyCell => {} CalcResult::EmptyCell | CalcResult::EmptyArg => {}
} };
}
CalcResult::Boolean(result)
}
if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value) /// XOR(logical1, [logical]*,...)
{ /// Logical1 is required, subsequent logical values are optional. Can be logical values, arrays, or references.
if current_result == short_circuit_value { /// The result of XOR is TRUE when the number of TRUE inputs is odd and FALSE when the number of TRUE inputs is even.
return CalcResult::Boolean(current_result); pub(crate) fn fn_xor(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let mut true_count = 0;
let mut false_count = 0;
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Boolean(b) => {
if b {
true_count += 1;
} else {
false_count += 1;
}
} }
} CalcResult::Number(value) => {
if value != 0.0 {
true_count += 1;
} else {
false_count += 1;
}
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
for row in left.row..(right.row + 1) {
for column in left.column..(right.column + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Boolean(b) => {
if b {
true_count += 1;
} else {
false_count += 1;
}
}
CalcResult::Number(value) => {
if value != 0.0 {
true_count += 1;
} else {
false_count += 1;
}
}
_ => {}
}
}
}
}
_ => {}
};
} }
if true_count == 0 && false_count == 0 {
if let Some(result) = result { return CalcResult::new_error(Error::VALUE, cell, "No booleans found".to_string());
CalcResult::Boolean(result)
} else {
CalcResult::new_error(
Error::VALUE,
cell,
"No logical values in argument list".to_string(),
)
} }
CalcResult::Boolean(true_count % 2 == 1)
} }
/// =SWITCH(expression, case1, value1, [case, value]*, [default]) /// =SWITCH(expression, case1, value1, [case, value]*, [default])

View File

@@ -25,8 +25,6 @@
#![doc = include_str!("../examples/formulas_and_errors.rs")] #![doc = include_str!("../examples/formulas_and_errors.rs")]
//! ``` //! ```
#![warn(clippy::print_stdout)]
pub mod calc_result; pub mod calc_result;
pub mod cell; pub mod cell;
pub mod expressions; pub mod expressions;

View File

@@ -8,15 +8,14 @@ use crate::{
cell::CellValue, cell::CellValue,
constants::{self, LAST_COLUMN, LAST_ROW}, constants::{self, LAST_COLUMN, LAST_ROW},
expressions::{ expressions::{
lexer::LexerMode,
parser::{ parser::{
move_formula::{move_formula, MoveContext}, move_formula::{move_formula, MoveContext},
stringify::{rename_defined_name_in_node, to_rc_format, to_string}, stringify::{to_rc_format, to_string},
Node, Parser, Node, Parser,
}, },
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary}, token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
types::*, types::*,
utils::{self, is_valid_column_number, is_valid_identifier, is_valid_row}, utils::{self, is_valid_column_number, is_valid_row},
}, },
formatter::{ formatter::{
format::{format_number, parse_formatted_number}, format::{format_number, parse_formatted_number},
@@ -73,7 +72,6 @@ pub(crate) enum CellState {
} }
/// A parsed formula for a defined name /// A parsed formula for a defined name
#[derive(Clone)]
pub(crate) enum ParsedDefinedName { pub(crate) enum ParsedDefinedName {
/// CellReference (`=C4`) /// CellReference (`=C4`)
CellReference(CellReferenceIndex), CellReference(CellReferenceIndex),
@@ -81,6 +79,9 @@ pub(crate) enum ParsedDefinedName {
RangeReference(Range), RangeReference(Range),
/// `=SomethingElse` /// `=SomethingElse`
InvalidDefinedNameFormula, InvalidDefinedNameFormula,
// TODO: Support constants in defined names
// TODO: Support formulas in defined names
// TODO: Support tables in defined names
} }
/// A dynamical IronCalc model. /// A dynamical IronCalc model.
@@ -416,40 +417,40 @@ impl Model {
// TODO: NOT IMPLEMENTED // TODO: NOT IMPLEMENTED
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string()) CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
} }
DefinedNameKind((name, scope)) => { VariableKind(defined_name) => {
if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) { println!("{:?}", defined_name);
let parsed_defined_name = self
.parsed_defined_names
.get(&(Some(cell.sheet), defined_name.to_lowercase())) // try getting local defined name
.or_else(|| {
self.parsed_defined_names
.get(&(None, defined_name.to_lowercase()))
}); // fallback to global
println!("Parsed: {:?}", defined_name);
if let Some(parsed_defined_name) = parsed_defined_name {
match parsed_defined_name { match parsed_defined_name {
ParsedDefinedName::CellReference(reference) => { ParsedDefinedName::CellReference(reference) => {
self.evaluate_cell(reference) self.evaluate_cell(*reference)
} }
ParsedDefinedName::RangeReference(range) => CalcResult::Range { ParsedDefinedName::RangeReference(range) => CalcResult::Range {
left: range.left, left: range.left,
right: range.right, right: range.right,
}, },
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error( ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
Error::NAME, Error::NIMPL,
cell, cell,
format!("Defined name \"{}\" is not a reference.", name), format!("Defined name \"{}\" is not a reference.", defined_name),
), ),
} }
} else { } else {
CalcResult::new_error( CalcResult::new_error(
Error::NAME, Error::NAME,
cell, cell,
format!("Defined name \"{}\" not found.", name), format!("Defined name \"{}\" not found.", defined_name),
) )
} }
} }
TableNameKind(s) => CalcResult::new_error(
Error::NAME,
cell,
format!("table name \"{}\" not supported.", s),
),
WrongVariableKind(s) => CalcResult::new_error(
Error::NAME,
cell,
format!("Variable name \"{}\" not found.", s),
),
CompareKind { kind, left, right } => { CompareKind { kind, left, right } => {
let l = self.evaluate_node_in_context(left, cell); let l = self.evaluate_node_in_context(left, cell);
if l.is_error() { if l.is_error() {
@@ -683,13 +684,6 @@ impl Model {
Err(format!("Invalid color: {}", color)) Err(format!("Invalid color: {}", color))
} }
/// Changes the visibility of a sheet
pub fn set_sheet_state(&mut self, sheet: u32, state: SheetState) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?;
worksheet.state = state;
Ok(())
}
/// Makes the grid lines in the sheet visible (`true`) or hidden (`false`) /// Makes the grid lines in the sheet visible (`true`) or hidden (`false`)
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> { pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
let worksheet = self.workbook.worksheet_mut(sheet)?; let worksheet = self.workbook.worksheet_mut(sheet)?;
@@ -865,11 +859,6 @@ impl Model {
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let defined_names = workbook
.get_defined_names_with_scope()
.iter()
.map(|s| (s.0.to_owned(), s.1))
.collect();
// add all tables // add all tables
// let mut tables = Vec::new(); // let mut tables = Vec::new();
// for worksheet in worksheets { // for worksheet in worksheets {
@@ -879,7 +868,7 @@ impl Model {
// } // }
// tables.push(tables_in_sheet); // tables.push(tables_in_sheet);
// } // }
let parser = Parser::new(worksheet_names, defined_names, workbook.tables.clone()); let parser = Parser::new(worksheet_names, workbook.tables.clone());
let cells = HashMap::new(); let cells = HashMap::new();
let locale = get_locale(&workbook.settings.locale) let locale = get_locale(&workbook.settings.locale)
.map_err(|_| "Invalid locale".to_string())? .map_err(|_| "Invalid locale".to_string())?
@@ -1040,7 +1029,7 @@ impl Model {
column: source.column, column: source.column,
}; };
let formula_str = move_formula( let formula_str = move_formula(
&self.parser.parse(formula, &cell_reference), &self.parser.parse(formula, &Some(cell_reference)),
&MoveContext { &MoveContext {
source_sheet_name: &source_sheet_name, source_sheet_name: &source_sheet_name,
row: source.row, row: source.row,
@@ -1148,7 +1137,7 @@ impl Model {
row: source.row, row: source.row,
column: source.column, column: source.column,
}; };
let formula = &self.parser.parse(formula_str, &cell_reference); let formula = &self.parser.parse(formula_str, &Some(cell_reference));
let cell_reference = CellReferenceRC { let cell_reference = CellReferenceRC {
sheet: target_sheet_name, sheet: target_sheet_name,
row: target.row, row: target.row,
@@ -1524,11 +1513,13 @@ impl Model {
column, column,
}; };
let shared_formulas = &mut worksheet.shared_formulas; let shared_formulas = &mut worksheet.shared_formulas;
let mut parsed_formula = self.parser.parse(formula, &cell_reference); let mut parsed_formula = self.parser.parse(formula, &Some(cell_reference.clone()));
// If the formula fails to parse try adding a parenthesis // If the formula fails to parse try adding a parenthesis
// SUM(A1:A3 => SUM(A1:A3) // SUM(A1:A3 => SUM(A1:A3)
if let Node::ParseErrorKind { .. } = parsed_formula { if let Node::ParseErrorKind { .. } = parsed_formula {
let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference); let new_parsed_formula = self
.parser
.parse(&format!("{})", formula), &Some(cell_reference));
match new_parsed_formula { match new_parsed_formula {
Node::ParseErrorKind { .. } => {} Node::ParseErrorKind { .. } => {}
_ => parsed_formula = new_parsed_formula, _ => parsed_formula = new_parsed_formula,
@@ -1607,42 +1598,6 @@ impl Model {
.set_cell_with_number(row, column, value, style) .set_cell_with_number(row, column, value, style)
} }
// Helper function that returns a defined name given the name and scope
fn get_parsed_defined_name(
&self,
name: &str,
scope: Option<u32>,
) -> Result<Option<ParsedDefinedName>, String> {
let name_upper = name.to_uppercase();
for (key, df) in &self.parsed_defined_names {
if key.1.to_uppercase() == name_upper && key.0 == scope {
return Ok(Some(df.clone()));
}
}
Ok(None)
}
// Returns the formula for a defined name
pub(crate) fn get_defined_name_formula(
&self,
name: &str,
scope: Option<u32>,
) -> Result<String, String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
return Ok(df.formula.clone());
}
}
Err("Defined name not found".to_string())
}
/// Gets the Excel Value (Bool, Number, String) of a cell /// Gets the Excel Value (Bool, Number, String) of a cell
/// ///
/// See also: /// See also:
@@ -2041,28 +1996,20 @@ impl Model {
scope: Option<u32>, scope: Option<u32>,
formula: &str, formula: &str,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_identifier(name) {
return Err("Invalid defined name".to_string());
};
let name_upper = name.to_uppercase(); let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names; let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
// if the defined name already exist return error // if the defined name already exist return error
for df in defined_names { for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id { if df.name.to_uppercase() == name_upper && df.sheet_id == scope {
return Err("Defined name already exists".to_string()); return Err("Defined name already exists".to_string());
} }
} }
self.workbook.defined_names.push(DefinedName { self.workbook.defined_names.push(DefinedName {
name: name.to_string(), name: name.to_string(),
formula: formula.to_string(), formula: formula.to_string(),
sheet_id, sheet_id: scope,
}); });
self.reset_parsed_structures(); self.reset_parsed_structures();
Ok(()) Ok(())
} }
@@ -2070,13 +2017,9 @@ impl Model {
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> { pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
let name_upper = name.to_uppercase(); let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names; let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
let mut index = None; let mut index = None;
for (i, df) in defined_names.iter().enumerate() { for (i, df) in defined_names.iter().enumerate() {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id { if df.name.to_uppercase() == name_upper && df.sheet_id == scope {
index = Some(i); index = Some(i);
} }
} }
@@ -2089,7 +2032,23 @@ impl Model {
} }
} }
/// Update defined name /// returns the formula for a defined name
pub fn get_defined_name_formula(
&self,
name: &str,
scope: Option<u32>,
) -> Result<String, String> {
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == scope {
return Ok(df.formula.clone());
}
}
Err("Defined name not found".to_string())
}
/// update defined name
pub fn update_defined_name( pub fn update_defined_name(
&mut self, &mut self,
name: &str, name: &str,
@@ -2098,64 +2057,18 @@ impl Model {
new_scope: Option<u32>, new_scope: Option<u32>,
new_formula: &str, new_formula: &str,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_identifier(new_name) {
return Err("Invalid defined name".to_string());
};
let name_upper = name.to_uppercase(); let name_upper = name.to_uppercase();
let new_name_upper = new_name.to_uppercase();
if name_upper != new_name_upper || scope != new_scope {
for key in self.parsed_defined_names.keys() {
if key.1.to_uppercase() == new_name_upper && key.0 == new_scope {
return Err("Defined name already exists".to_string());
}
}
}
let defined_names = &self.workbook.defined_names; let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
let new_sheet_id = match new_scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
let mut index = None; let mut index = None;
for (i, df) in defined_names.iter().enumerate() { for (i, df) in defined_names.iter().enumerate() {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id { if df.name.to_uppercase() == name_upper && df.sheet_id == scope {
index = Some(i); index = Some(i);
} }
} }
if let Some(i) = index { if let Some(i) = index {
if let Some(df) = self.workbook.defined_names.get_mut(i) { if let Some(df) = self.workbook.defined_names.get_mut(i) {
if new_name != df.name {
// We need to rename the name in every formula:
// Parse all formulas with the old name
// All internal formulas are R1C1
self.parser.set_lexer_mode(LexerMode::R1C1);
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_defined_name_in_node(&mut t, name, scope, 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);
}
df.name = new_name.to_string(); df.name = new_name.to_string();
df.sheet_id = new_sheet_id; df.sheet_id = new_scope;
df.formula = new_formula.to_string(); df.formula = new_formula.to_string();
self.reset_parsed_structures(); self.reset_parsed_structures();
} }

View File

@@ -85,14 +85,14 @@ impl Model {
let worksheets = &self.workbook.worksheets; let worksheets = &self.workbook.worksheets;
for worksheet in worksheets { for worksheet in worksheets {
let shared_formulas = &worksheet.shared_formulas; let shared_formulas = &worksheet.shared_formulas;
let cell_reference = CellReferenceRC { let cell_reference = &Some(CellReferenceRC {
sheet: worksheet.get_name(), sheet: worksheet.get_name(),
row: 1, row: 1,
column: 1, column: 1,
}; });
let mut parse_formula = Vec::new(); let mut parse_formula = Vec::new();
for formula in shared_formulas { for formula in shared_formulas {
let t = self.parser.parse(formula, &cell_reference); let t = self.parser.parse(formula, cell_reference);
parse_formula.push(t); parse_formula.push(t);
} }
self.parsed_formulas.push(parse_formula); self.parsed_formulas.push(parse_formula);
@@ -144,14 +144,8 @@ impl Model {
/// Reparses all formulas and defined names /// Reparses all formulas and defined names
pub(crate) fn reset_parsed_structures(&mut self) { 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 self.parser
.set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names); .set_worksheets(self.workbook.get_worksheet_names());
self.parsed_formulas = vec![]; self.parsed_formulas = vec![];
self.parse_formulas(); self.parse_formulas();
self.parsed_defined_names = HashMap::new(); self.parsed_defined_names = HashMap::new();
@@ -268,11 +262,11 @@ impl Model {
// We use iter because the default would be a mut_iter and we don't need a mutable reference // 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; let worksheets = &mut self.workbook.worksheets;
for worksheet in worksheets { for worksheet in worksheets {
let cell_reference = &CellReferenceRC { let cell_reference = &Some(CellReferenceRC {
sheet: worksheet.get_name(), sheet: worksheet.get_name(),
row: 1, row: 1,
column: 1, column: 1,
}; });
let mut formulas = Vec::new(); let mut formulas = Vec::new();
for formula in &worksheet.shared_formulas { for formula in &worksheet.shared_formulas {
let mut t = self.parser.parse(formula, cell_reference); let mut t = self.parser.parse(formula, cell_reference);
@@ -394,7 +388,7 @@ impl Model {
let parsed_formulas = Vec::new(); let parsed_formulas = Vec::new();
let worksheets = &workbook.worksheets; let worksheets = &workbook.worksheets;
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
let parser = Parser::new(worksheet_names, vec![], HashMap::new()); let parser = Parser::new(worksheet_names, HashMap::new());
let cells = HashMap::new(); let cells = HashMap::new();
// FIXME: Add support for display languages // FIXME: Add support for display languages

View File

@@ -19,7 +19,6 @@ mod test_fn_formulatext;
mod test_fn_if; mod test_fn_if;
mod test_fn_maxifs; mod test_fn_maxifs;
mod test_fn_minifs; mod test_fn_minifs;
mod test_fn_or_xor;
mod test_fn_product; mod test_fn_product;
mod test_fn_rept; mod test_fn_rept;
mod test_fn_sum; mod test_fn_sum;
@@ -47,6 +46,7 @@ pub(crate) mod util;
mod engineering; mod engineering;
mod test_fn_offset; mod test_fn_offset;
mod test_fn_or;
mod test_number_format; mod test_number_format;
mod test_escape_quotes; mod test_escape_quotes;

View File

@@ -0,0 +1,36 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_or() {
let mut model = new_empty_model();
model._set("A1", "=OR(1, 0)");
model._set("A2", "=OR(0, 0)");
model._set("A3", "=OR(true, false)");
model._set("A4", "=OR(false, false)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"FALSE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
}
#[test]
fn fn_or_no_arguments() {
let mut model = new_empty_model();
model._set("A1", "=OR()");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
}
#[test]
fn fn_or_missing_arguments() {
let mut model = new_empty_model();
model._set("A1", "=OR(,)");
model._set("A2", "=OR(,1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"FALSE");
assert_eq!(model._get_text("A2"), *"TRUE");
}

View File

@@ -1,204 +0,0 @@
#![allow(clippy::unwrap_used)]
#![allow(clippy::print_stdout)]
use crate::test::util::new_empty_model;
// These tests are grouped because in many cases XOR and OR have similar behaviour.
// Test specific to xor
#[test]
fn fn_xor() {
let mut model = new_empty_model();
model._set("A1", "=XOR(1, 1, 1, 0, 0)");
model._set("A2", "=XOR(1, 1, 0, 0, 0)");
model._set("A3", "=XOR(TRUE, TRUE, TRUE, FALSE, FALSE)");
model._set("A4", "=XOR(TRUE, TRUE, FALSE, FALSE, FALSE)");
model._set("A5", "=XOR(FALSE, FALSE, FALSE, FALSE, FALSE)");
model._set("A6", "=XOR(TRUE, TRUE)");
model._set("A7", "=XOR(0,0,0)");
model._set("A8", "=XOR(0,0,1)");
model._set("A9", "=XOR(0,1,0)");
model._set("A10", "=XOR(0,1,1)");
model._set("A11", "=XOR(1,0,0)");
model._set("A12", "=XOR(1,0,1)");
model._set("A13", "=XOR(1,1,0)");
model._set("A14", "=XOR(1,1,1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"FALSE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
assert_eq!(model._get_text("A5"), *"FALSE");
assert_eq!(model._get_text("A6"), *"FALSE");
assert_eq!(model._get_text("A7"), *"FALSE");
assert_eq!(model._get_text("A8"), *"TRUE");
assert_eq!(model._get_text("A9"), *"TRUE");
assert_eq!(model._get_text("A10"), *"FALSE");
assert_eq!(model._get_text("A11"), *"TRUE");
assert_eq!(model._get_text("A12"), *"FALSE");
assert_eq!(model._get_text("A13"), *"FALSE");
assert_eq!(model._get_text("A14"), *"TRUE");
}
#[test]
fn fn_or() {
let mut model = new_empty_model();
model._set("A1", "=OR(1, 1, 1, 0, 0)");
model._set("A2", "=OR(1, 1, 0, 0, 0)");
model._set("A3", "=OR(TRUE, TRUE, TRUE, FALSE, FALSE)");
model._set("A4", "=OR(TRUE, TRUE, FALSE, FALSE, FALSE)");
model._set("A5", "=OR(FALSE, FALSE, FALSE, FALSE, FALSE)");
model._set("A6", "=OR(TRUE, TRUE)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"TRUE");
assert_eq!(model._get_text("A2"), *"TRUE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"TRUE");
assert_eq!(model._get_text("A5"), *"FALSE");
assert_eq!(model._get_text("A6"), *"TRUE");
}
#[test]
fn fn_or_xor() {
inner("or");
inner("xor");
fn inner(func: &str) {
println!("Testing function: {func}");
let mut model = new_empty_model();
// Text args
model._set("A1", &format!(r#"={func}("")"#));
model._set("A2", &format!(r#"={func}("", "")"#));
model._set("A3", &format!(r#"={func}("", TRUE)"#));
model._set("A4", &format!(r#"={func}("", FALSE)"#));
model._set("A5", &format!("={func}(FALSE, TRUE)"));
model._set("A6", &format!("={func}(FALSE, FALSE)"));
model._set("A7", &format!("={func}(TRUE, FALSE)"));
// Reference to empty cell, plus true argument
model._set("A8", &format!("={func}(Z99, 1)"));
// Reference to empty cell/range
model._set("A9", &format!("={func}(Z99)"));
model._set("A10", &format!("={func}(X99:Z99"));
// Reference to cell with reference to empty range
model._set("B11", "=X99:Z99");
model._set("A11", &format!("={func}(B11)"));
// Reference to cell with non-empty range
model._set("X12", "1");
model._set("B12", "=X12:Z12");
model._set("A12", &format!("={func}(B12)"));
// Reference to text cell
model._set("B13", "some_text");
model._set("A13", &format!("={func}(B13)"));
model._set("A14", &format!("={func}(B13, 0)"));
model._set("A15", &format!("={func}(B13, 1)"));
// Reference to Implicit intersection
model._set("X16", "1");
model._set("B16", "=@X15:X16");
model._set("A16", &format!("={func}(B16)"));
// Non-empty range
model._set("B17", "1");
model._set("A17", &format!("={func}(B17:C17)"));
// Non-empty range with text
model._set("B18", "text");
model._set("A18", &format!("={func}(B18:C18)"));
// Non-empty range with text and number
model._set("B19", "text");
model._set("C19", "1");
model._set("A19", &format!("={func}(B19:C19)"));
// range with error
model._set("B20", "=1/0");
model._set("A20", &format!("={func}(B20:C20)"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"#VALUE!");
assert_eq!(model._get_text("A2"), *"#VALUE!");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
assert_eq!(model._get_text("A5"), *"TRUE");
assert_eq!(model._get_text("A6"), *"FALSE");
assert_eq!(model._get_text("A7"), *"TRUE");
assert_eq!(model._get_text("A8"), *"TRUE");
assert_eq!(model._get_text("A9"), *"#VALUE!");
assert_eq!(model._get_text("A10"), *"#VALUE!");
assert_eq!(model._get_text("A11"), *"#VALUE!");
// TODO: This one depends on spill behaviour which isn't implemented yet
// assert_eq!(model._get_text("A12"), *"TRUE");
assert_eq!(model._get_text("A13"), *"#VALUE!");
assert_eq!(model._get_text("A14"), *"FALSE");
assert_eq!(model._get_text("A15"), *"TRUE");
// TODO: This one depends on @ implicit intersection behaviour which isn't implemented yet
// assert_eq!(model._get_text("A16"), *"TRUE");
assert_eq!(model._get_text("A17"), *"TRUE");
assert_eq!(model._get_text("A18"), *"#VALUE!");
assert_eq!(model._get_text("A19"), *"TRUE");
assert_eq!(model._get_text("A20"), *"#DIV/0!");
}
}
#[test]
fn fn_or_xor_no_arguments() {
inner("or");
inner("xor");
fn inner(func: &str) {
println!("Testing function: {func}");
let mut model = new_empty_model();
model._set("A1", &format!("={}()", func));
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
}
}
#[test]
fn fn_or_xor_missing_arguments() {
inner("or");
inner("xor");
fn inner(func: &str) {
println!("Testing function: {func}");
let mut model = new_empty_model();
model._set("A1", &format!("={func}(,)"));
model._set("A2", &format!("={func}(,1)"));
model._set("A3", &format!("={func}(1,)"));
model._set("A4", &format!("={func}(,B1)"));
model._set("A5", &format!("={func}(,B1:B4)"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"FALSE");
assert_eq!(model._get_text("A2"), *"TRUE");
assert_eq!(model._get_text("A3"), *"TRUE");
assert_eq!(model._get_text("A4"), *"FALSE");
assert_eq!(model._get_text("A5"), *"FALSE");
}
}

View File

@@ -15,7 +15,6 @@ mod test_on_paste_styles;
mod test_paste_csv; mod test_paste_csv;
mod test_rename_sheet; mod test_rename_sheet;
mod test_row_column; mod test_row_column;
mod test_sheet_state;
mod test_styles; mod test_styles;
mod test_to_from_bytes; mod test_to_from_bytes;
mod test_undo_redo; mod test_undo_redo;

View File

@@ -6,393 +6,35 @@ use crate::UserModel;
fn create_defined_name() { fn create_defined_name() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap(); model.set_user_input(0, 1, 1, "42").unwrap();
model model.new_defined_name("myName", None, "$A$1").unwrap();
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
model.set_user_input(0, 5, 7, "=myName").unwrap(); model.set_user_input(0, 5, 7, "=myName").unwrap();
assert_eq!( assert_eq!(
model.get_formatted_cell_value(0, 5, 7), model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string()) Ok("42".to_string())
); );
// rename it
model
.update_defined_name("myName", None, "myName", None, "$A$1*2")
.unwrap();
assert_eq!( assert_eq!(
model.get_defined_name_list(), model.get_formatted_cell_value(0, 5, 7),
vec![("myName".to_string(), None, "Sheet1!$A$1".to_string())] Ok("42".to_string())
); );
// delete it // delete it
model.delete_defined_name("myName", None).unwrap(); model.delete_defined_name("myName", None).unwrap();
assert_eq!( assert_eq!(
model.get_formatted_cell_value(0, 5, 7), model.get_formatted_cell_value(0, 5, 7),
Ok("#NAME?".to_string()) Ok("#REF!".to_string())
);
assert_eq!(model.get_defined_name_list().len(), 0);
model.undo().unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
); );
} }
#[test] #[test]
fn scopes() { fn rename_defined_name() {}
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
// Global
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
model.set_user_input(0, 5, 7, "=myName").unwrap();
// Local to Sheet2
model.new_sheet().unwrap();
model.set_user_input(1, 2, 1, "145").unwrap();
model
.new_defined_name("myName", Some(1), "Sheet2!$A$2")
.unwrap();
model.set_user_input(1, 8, 8, "=myName").unwrap();
// Sheet 3
model.new_sheet().unwrap();
model.set_user_input(2, 2, 2, "=myName").unwrap();
// Global
assert_eq!(
model.get_formatted_cell_value(0, 5, 7),
Ok("42".to_string())
);
assert_eq!(
model.get_formatted_cell_value(1, 8, 8),
Ok("145".to_string())
);
assert_eq!(
model.get_formatted_cell_value(2, 2, 2),
Ok("42".to_string())
);
}
#[test] #[test]
fn delete_sheet() { fn delete_sheet() {}
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model.new_sheet().unwrap();
model
.new_defined_name("myName", Some(1), "Sheet1!$A$1")
.unwrap();
model
.set_user_input(1, 2, 1, r#"=CONCATENATE(MyName, " my world!")"#)
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("#NAME?".to_string())
);
assert_eq!(
model.get_formatted_cell_value(1, 2, 1),
Ok("Hello my world!".to_string())
);
model.delete_sheet(0).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("#NAME?".to_string())
);
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok(r#"=CONCATENATE(MyName," my world!")"#.to_string())
);
}
#[test] #[test]
fn change_scope() { fn change_scope() {}
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model.new_sheet().unwrap();
model
.new_defined_name("myName", Some(1), "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("#NAME?".to_string())
);
model
.update_defined_name("myName", Some(1), "myName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
}
#[test]
fn rename_defined_name() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.set_user_input(0, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
.unwrap();
model.new_sheet().unwrap();
model
.new_defined_name("myName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
model
.update_defined_name("myName", None, "newName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 2, 1),
Ok("Hello world!".to_string())
);
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok(r#"=CONCATENATE(newName," world!")"#.to_string())
);
}
#[test]
fn rename_defined_name_operations() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "123").unwrap();
model
.new_defined_name("answer", None, "Sheet1!$A$1")
.unwrap();
model
.set_user_input(0, 2, 1, "=IF(answer<2, answer*2, answer^2)")
.unwrap();
model
.set_user_input(0, 3, 1, "=badDunction(-answer)")
.unwrap();
model.new_sheet().unwrap();
model.set_user_input(1, 1, 1, "78").unwrap();
model
.new_defined_name("answer", Some(1), "Sheet1!$A$1")
.unwrap();
model.set_user_input(1, 3, 1, "=answer").unwrap();
model
.update_defined_name("answer", None, "respuesta", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok("=IF(respuesta<2,respuesta*2,respuesta^2)".to_string())
);
assert_eq!(
model.get_cell_content(0, 3, 1),
Ok("=badDunction(-respuesta)".to_string())
);
// A defined name with the same name but different scope
assert_eq!(model.get_cell_content(1, 3, 1), Ok("=answer".to_string()));
}
#[test]
fn rename_defined_name_string_operations() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model.set_user_input(0, 1, 2, "World").unwrap();
model
.new_defined_name("hello", None, "Sheet1!$A$1")
.unwrap();
model
.new_defined_name("world", None, "Sheet1!$B$1")
.unwrap();
model.set_user_input(0, 2, 1, "=hello&world").unwrap();
model
.update_defined_name("hello", None, "HolaS", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_cell_content(0, 2, 1),
Ok("=HolaS&world".to_string())
);
}
#[test]
fn invalid_names() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
// spaces
assert_eq!(
model.new_defined_name("A real", None, "Sheet1!$A$1"),
Err("Invalid defined name".to_string())
);
// Starts with number
assert_eq!(
model.new_defined_name("2real", None, "Sheet1!$A$1"),
Err("Invalid defined name".to_string())
);
// Updating also fails
assert_eq!(
model.update_defined_name("MyName", None, "My Name", None, "Sheet1!$A$1"),
Err("Invalid defined name".to_string())
);
}
#[test]
fn already_existing() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
model
.new_defined_name("Another", None, "Sheet1!$A$1")
.unwrap();
// Can't create a new name with the same name
assert_eq!(
model.new_defined_name("MyName", None, "Sheet1!$A$2"),
Err("Defined name already exists".to_string())
);
// Can't update one into an existing
assert_eq!(
model.update_defined_name("Another", None, "MyName", None, "Sheet1!$A$1"),
Err("Defined name already exists".to_string())
);
}
#[test]
fn invalid_sheet() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.new_defined_name("Mything", Some(2), "Sheet1!$A$1"),
Err("Invalid sheet index".to_string())
);
assert_eq!(
model.update_defined_name("MyName", None, "MyName", Some(2), "Sheet1!$A$1"),
Err("Invalid sheet index".to_string())
);
assert_eq!(
model.update_defined_name("MyName", Some(9), "YourName", None, "Sheet1!$A$1"),
Err("Invalid sheet index".to_string())
);
}
#[test]
fn invalid_formula() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model.new_defined_name("MyName", None, "A1").unwrap();
model.set_user_input(0, 1, 2, "=MyName").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("#NAME?".to_string())
);
}
#[test]
fn undo_redo() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap();
model.set_user_input(0, 2, 1, "Hola").unwrap();
model.set_user_input(0, 1, 2, r#"=MyName&"!""#).unwrap();
model
.new_defined_name("MyName", None, "Sheet1!$A$1")
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hello!".to_string())
);
model.undo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 0);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("#NAME?".to_string())
);
model.redo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hello!".to_string())
);
model
.update_defined_name("MyName", None, "MyName", None, "Sheet1!$A$2")
.unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hola!".to_string())
);
model.undo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hello!".to_string())
);
model.redo().unwrap();
assert_eq!(model.get_defined_name_list().len(), 1);
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("Hola!".to_string())
);
let send_queue = model.flush_send_queue();
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
model2.apply_external_diffs(&send_queue).unwrap();
assert_eq!(model2.get_defined_name_list().len(), 1);
assert_eq!(
model2.get_formatted_cell_value(0, 1, 2),
Ok("Hola!".to_string())
);
}

View File

@@ -91,6 +91,7 @@ fn insert_remove_columns() {
let mut model = UserModel::from_model(model); let mut model = UserModel::from_model(model);
// column E // column E
let column_width = model.get_column_width(0, 5).unwrap(); let column_width = model.get_column_width(0, 5).unwrap();
println!("{column_width}");
// Insert some data in row 5 (and change the style) in E1 // Insert some data in row 5 (and change the style) in E1
assert!(model.set_user_input(0, 1, 5, "100$").is_ok()); assert!(model.set_user_input(0, 1, 5, "100$").is_ok());

View File

@@ -1,57 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::UserModel;
#[test]
fn basic_tests() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
// add three more sheets
model.new_sheet().unwrap();
model.new_sheet().unwrap();
model.new_sheet().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.len(), 4);
for sheet in &info {
assert_eq!(sheet.state, "visible".to_string());
}
model.set_selected_sheet(2).unwrap();
assert_eq!(info.get(2).unwrap().name, "Sheet3".to_string());
model.hide_sheet(2).unwrap();
let info = model.get_worksheets_properties();
assert_eq!(model.get_selected_sheet(), 3);
assert_eq!(info.get(2).unwrap().state, "hidden".to_string());
model.undo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(2).unwrap().state, "visible".to_string());
model.redo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(2).unwrap().state, "hidden".to_string());
model.set_selected_sheet(3).unwrap();
model.hide_sheet(3).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
model.unhide_sheet(2).unwrap();
model.unhide_sheet(3).unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.len(), 4);
for sheet in &info {
assert_eq!(sheet.state, "visible".to_string());
}
model.undo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(3).unwrap().state, "hidden".to_string());
model.redo().unwrap();
let info = model.get_worksheets_properties();
assert_eq!(info.get(3).unwrap().state, "visible".to_string());
}

View File

@@ -293,9 +293,7 @@ impl Model {
Node::EmptyArgKind => None, Node::EmptyArgKind => None,
Node::InvalidFunctionKind { .. } => None, Node::InvalidFunctionKind { .. } => None,
Node::ArrayKind(_) => None, Node::ArrayKind(_) => None,
Node::DefinedNameKind(_) => None, Node::VariableKind(_) => None,
Node::TableNameKind(_) => None,
Node::WrongVariableKind(_) => None,
Node::CompareKind { .. } => None, Node::CompareKind { .. } => None,
Node::OpPowerKind { .. } => None, Node::OpPowerKind { .. } => None,
} }

View File

@@ -13,7 +13,7 @@ use crate::{
}, },
model::Model, model::Model,
types::{ types::{
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState, Alignment, BorderItem, CellType, Col, DefinedName, HorizontalAlignment, SheetProperties,
Style, VerticalAlignment, Style, VerticalAlignment,
}, },
utils::is_valid_hex_color, utils::is_valid_hex_color,
@@ -440,48 +440,6 @@ impl UserModel {
Ok(()) Ok(())
} }
/// Hides sheet by index
///
/// See also:
/// * [Model::set_sheet_state]
/// * [UserModel::unhide_sheet]
pub fn hide_sheet(&mut self, sheet: u32) -> Result<(), String> {
let sheet_count = self.model.workbook.worksheets.len() as u32;
for index in 1..sheet_count {
let sheet_index = (sheet + index) % sheet_count;
if self.model.workbook.worksheet(sheet_index)?.state == SheetState::Visible {
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
view.sheet = sheet_index;
};
break;
}
}
let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
self.push_diff_list(vec![Diff::SetSheetState {
index: sheet,
new_value: SheetState::Hidden,
old_value,
}]);
self.model.set_sheet_state(sheet, SheetState::Hidden)?;
Ok(())
}
/// Un hides sheet by index
///
/// See also:
/// * [Model::set_sheet_state]
/// * [UserModel::hide_sheet]
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<(), String> {
let old_value = self.model.workbook.worksheet(sheet)?.state.clone();
self.push_diff_list(vec![Diff::SetSheetState {
index: sheet,
new_value: SheetState::Visible,
old_value,
}]);
self.model.set_sheet_state(sheet, SheetState::Visible)?;
Ok(())
}
/// Sets sheet color /// Sets sheet color
/// ///
/// Note: an empty string will remove the color /// Note: an empty string will remove the color
@@ -1735,8 +1693,8 @@ impl UserModel {
} }
/// Returns the list of defined names /// Returns the list of defined names
pub fn get_defined_name_list(&self) -> Vec<(String, Option<u32>, String)> { pub fn get_defined_name_list(&self) -> Vec<DefinedName> {
self.model.workbook.get_defined_names_with_scope() self.model.workbook.defined_names.clone()
} }
/// Delete an existing defined name /// Delete an existing defined name
@@ -1790,8 +1748,6 @@ impl UserModel {
new_formula: new_formula.to_string(), new_formula: new_formula.to_string(),
}]; }];
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.model
.update_defined_name(name, scope, new_name, new_scope, new_formula)?;
self.evaluate_if_not_paused(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
@@ -1966,41 +1922,20 @@ impl UserModel {
} => { } => {
self.model.set_show_grid_lines(*sheet, *old_value)?; self.model.set_show_grid_lines(*sheet, *old_value)?;
} }
Diff::CreateDefinedName { Diff::CreateDefinedName { name, scope, value } => todo!(),
name,
scope,
value: _,
} => {
self.model.delete_defined_name(name, *scope)?;
}
Diff::DeleteDefinedName { Diff::DeleteDefinedName {
name, name,
scope, scope,
old_value, old_value,
} => { } => todo!(),
self.model.new_defined_name(name, *scope, old_value)?;
}
Diff::UpdateDefinedName { Diff::UpdateDefinedName {
name, name,
scope, scope,
old_formula, old_formula,
new_name, new_name,
new_scope, new_scope,
new_formula: _, new_formula,
} => { } => todo!(),
self.model.update_defined_name(
new_name,
*new_scope,
name,
*scope,
old_formula,
)?;
}
Diff::SetSheetState {
index,
old_value,
new_value: _,
} => self.model.set_sheet_state(*index, old_value.clone())?,
} }
} }
if needs_evaluation { if needs_evaluation {
@@ -2128,33 +2063,20 @@ impl UserModel {
} => { } => {
self.model.set_show_grid_lines(*sheet, *new_value)?; self.model.set_show_grid_lines(*sheet, *new_value)?;
} }
Diff::CreateDefinedName { name, scope, value } => { Diff::CreateDefinedName { name, scope, value } => todo!(),
self.model.new_defined_name(name, *scope, value)?
}
Diff::DeleteDefinedName { Diff::DeleteDefinedName {
name, name,
scope, scope,
old_value: _, old_value,
} => self.model.delete_defined_name(name, *scope)?, } => todo!(),
Diff::UpdateDefinedName { Diff::UpdateDefinedName {
name, name,
scope, scope,
old_formula: _, old_formula,
new_name, new_name,
new_scope, new_scope,
new_formula, new_formula,
} => self.model.update_defined_name( } => todo!(),
name,
*scope,
new_name,
*new_scope,
new_formula,
)?,
Diff::SetSheetState {
index,
old_value: _,
new_value,
} => self.model.set_sheet_state(*index, new_value.clone())?,
} }
} }

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use crate::types::{Cell, Col, Row, SheetState, Style}; use crate::types::{Cell, Col, Row, Style};
#[derive(Clone, Encode, Decode)] #[derive(Clone, Encode, Decode)]
pub(crate) struct RowData { pub(crate) struct RowData {
@@ -104,11 +104,6 @@ pub(crate) enum Diff {
old_value: String, old_value: String,
new_value: String, new_value: String,
}, },
SetSheetState {
index: u32,
old_value: SheetState,
new_value: SheetState,
},
SetShowGridLines { SetShowGridLines {
sheet: u32, sheet: u32,
old_value: bool, old_value: bool,

View File

@@ -27,27 +27,4 @@ impl Workbook {
.get_mut(worksheet_index as usize) .get_mut(worksheet_index as usize)
.ok_or_else(|| "Invalid sheet index".to_string()) .ok_or_else(|| "Invalid sheet index".to_string())
} }
/// Returns the a list of defined names in the workbook with their scope
pub(crate) fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>, String)> {
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
let defined_names = self
.defined_names
.iter()
.map(|dn| {
let index = dn
.sheet_id
.and_then(|sheet_id| {
// returns an Option<usize>
sheet_id_index.iter().position(|&x| x == sheet_id)
})
// convert Option<usize> to Option<u32>
.map(|pos| pos as u32);
(dn.name.clone(), index, dn.formula.clone())
})
.collect::<Vec<_>>();
defined_names
}
} }

View File

@@ -114,16 +114,6 @@ impl Model {
self.model.delete_sheet(sheet).map_err(to_js_error) self.model.delete_sheet(sheet).map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "hideSheet")]
pub fn hide_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
self.model.hide_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "unhideSheet")]
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
self.model.unhide_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "renameSheet")] #[wasm_bindgen(js_name = "renameSheet")]
pub fn rename_sheet(&mut self, sheet: u32, name: &str) -> Result<(), JsError> { pub fn rename_sheet(&mut self, sheet: u32, name: &str) -> Result<(), JsError> {
self.model.rename_sheet(sheet, name).map_err(to_js_error) self.model.rename_sheet(sheet, name).map_err(to_js_error)
@@ -563,16 +553,16 @@ impl Model {
#[wasm_bindgen(js_name = "getDefinedNameList")] #[wasm_bindgen(js_name = "getDefinedNameList")]
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> { pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
let data: Vec<DefinedName> = self let data: Vec<DefinedName> =
.model self.model
.get_defined_name_list() .get_defined_name_list()
.iter() .iter()
.map(|s| DefinedName { .map(|s| DefinedName {
name: s.0.to_owned(), name: s.name.to_string(),
scope: s.1, scope: s.sheet_id,
formula: s.2.to_owned(), formula: s.formula.to_string(),
}) }).collect();
.collect(); // Ok(data)
serde_wasm_bindgen::to_value(&data).map_err(|e| to_js_error(e.to_string())) serde_wasm_bindgen::to_value(&data).map_err(|e| to_js_error(e.to_string()))
} }

View File

@@ -113,7 +113,6 @@ export interface WorksheetProperties {
name: string; name: string;
color: string; color: string;
sheet_id: number; sheet_id: number;
state: string;
} }
interface CellStyleFill { interface CellStyleFill {

View File

@@ -58,18 +58,6 @@ export default defineConfig({
text: "Error Types", text: "Error Types",
link: "/features/error-types", link: "/features/error-types",
}, },
{
text: "Value Types",
link: "/features/value-types",
},
{
text: "Optional Arguments",
link: "/features/optional-arguments",
},
{
text: "Units",
link: "/features/units",
},
{ {
text: "Unsupported Features", text: "Unsupported Features",
link: "/features/unsupported-features", link: "/features/unsupported-features",

View File

@@ -7,39 +7,31 @@ lang: en-US
# Error Types # Error Types
::: warning ::: warning
**Note:** This draft page is under construction 🚧 **Note:** This page is in construction 🚧
::: :::
The result of a formula is sometimes an _error_. In some situations those errors are expected and your formulas might be dealing with them. When working with formulas, you may encounter these common errors:
The error `#N/A` might signal that there is no data to evaluate the formula yet. Maybe the payroll has not been introduced for that month just yet.
Some other errors like `#SPILL!`, `#CIRC!` or `#ERROR!` signal an error in your spreadsheet logic and must be corrected. ---
The first kind of errors or 'common errors' are found in other spreadsheet engines like Excel while other errors like `#ERROR!` or `#N/IMPL` are particular to IronCalc. ### **`#ERROR!`**
## Common Errors **Cause:** General formula issue, like syntax errors or invalid references.
**Fix:** Check the formula for mistakes or invalid cell references.
---
### **`#VALUE!`** ### **`#VALUE!`**
It might be caused by mismatched data types (e.g., text used where numbers are expected): **Cause:** Mismatched data types (e.g., text used where numbers are expected).
**Fix:** Ensure input types are correct; convert text to numbers if needed.
``` ---
=5+"two"
```
The engine doesn't know how to add the number `5` to the string `two` resulting in a `#VALUE!`.
It is an actual error in your spreadsheet. It indicates that the formula isnt working as intended.
### **`#DIV/0!`** ### **`#DIV/0!`**
Division by zero or an empty cell: **Cause:** Division by zero or an empty cell.
**Fix:** Ensure the denominator isnt zero or blank. Use `IF` to handle such cases:
```
=1/0
```
Usually this is an error. However, in cases where a denominator might be blank (e.g., data not yet filled in), this could be expected. Use `IFERROR` or `IF` to handle it:
``` ```
=IF(B1=0, "N/A", A1/B1) =IF(B1=0, "N/A", A1/B1)
@@ -47,42 +39,23 @@ Usually this is an error. However, in cases where a denominator might be blank (
### **`#NAME?`** ### **`#NAME?`**
Found when a name is not recognized. Maybe a misspelled name for a function or a reference to a previously defined name that has since been deleted: **Cause:** Unrecognized text in the formula (e.g., misspelled function names or undefined named ranges).
**Fix:** Correct spelling or define the missing name.
```
=UNKNOWN_FUNCTION(A1)
```
This indicates an error in your spreadsheet logic.
### **`#REF!`** ### **`#REF!`**
Indicates an invalid cell reference, often from deleting cells used in a formula. **Cause:** Invalid cell reference, often from deleting cells used in a formula.
**Fix:** Update the formula with correct references.
They can appear as a result of a computation or in a formula. Example:
```
=Sheet34!A1
```
If `Sheet34` doesn't exist it will return `#REF!`
This is a genuine error. It indicates that part of your formula references a cell or range that is missing.
### **`#NUM!`** ### **`#NUM!`**
Invalid numeric operation (e.g., calculating the square root of a negative number). **Cause:** Invalid numeric operation (e.g., calculating a square root of a negative number).
Adjust the formula to ensure valid numeric operations. **Fix:** Adjust the formula to ensure valid numeric operations.
Sometimes a `#NUM!` error might be expected, signalling to the user that some parameter is out of scope.
### **`#N/A`** ### **`#N/A`**
A value is not available, often in lookup functions like VLOOKUP. **Cause:** A value is not available, often in lookup functions like VLOOKUP.
**Fix:** Ensure the lookup value exists or use IFNA() to handle missing values:
This is frequently not an error in your spreadsheet logic.
You can produce a prettier answer using the [`IFNA`](/functions/information/isna) formula:
``` ```
=IFNA(VLOOKUP(A1, B1:C10, 2, FALSE), "Not Found") =IFNA(VLOOKUP(A1, B1:C10, 2, FALSE), "Not Found")
@@ -90,62 +63,17 @@ You can produce a prettier answer using the [`IFNA`](/functions/information/isna
### **`#NULL!`** ### **`#NULL!`**
Incorrect range operator in a formula (e.g., missing a colon between cell references). **Cause:** Incorrect range operator in a formula (e.g., missing a colon between cell references).
**Fix:** Use correct range operators (e.g., A1:A10).
### **`#SPILL!`**
A cell in a formula will overwrite content in other cells.
This cannot happen right now in IronCalc as formulas don't spill yet.
### **`#CIRC!`** ### **`#CIRC!`**
Circular reference. This is an error in your spreadsheet and must be fixed. **Cause:** Circular reference.
It means that during the course of a computation, a circular dependency was found. **Fix:** Remove the circular reference.
A circular dependency is a dependency of a formula on itself.
For instance, in the cell `A1` the formula `=A1*2` is a circular dependency.
Other spreadsheet engines use circular dependencies to do "loop computations", run "sensitivity analysis" or "goal seek".
IronCalc doesn't support any of those at the moment.
## IronCalc specific errors
### **`#ERROR!`**
General formula issue, like syntax errors or invalid references.
In general, Excel does not let you enter incorrect formulas, but IronCalc will.
This will make your workbook imcompatible with Excel.
Typical examples might be an incomplete formula, such as `=A1+`, or a function call with too few arguments, such as `=FV(1,2)`.
### **`#N/IMPL!`**
A particular feature is not yet implemented in IronCalc
Check if there is a [Github](https://github.com/ironcalc) ticket or contact us via [email](mailto:hello@ironcalc.com) or [Discord](https://discord.com/invite/zZYWfh3RHJ).
## Error propagation
Some errors are created by some formulas. For instance, the function `SQRT` can create the error `#NUM!`, but can't ceate the error `#DIV/0`.
Once an error is created it is normally _propagated_ by all the formulas. So if cell `C3` evaluates to `#ERROR!`, then the formula
`=SQRT(C3)` will return `#ERROR!`.
Not all functions propagate errors in their arguments. For instance the function `IF(condition, if_true, if_false)` will only propagate an error in the `if_false` argument if the `condition` is `FALSE`. This is called _lazy evaluation_ - the function `IF` is _lazy_ because it only evaluates the arguments when needed. The opposite of lazy evaulaution is called _eager evaluation_.
Some functions also expect an error as an argument like [`ERROR.TYPE`](/functions/information/error.type) and will not propagate the error.
## See also ### **`#####`**
The following functions are convenient when working with errors **Cause:** The column isnt wide enough to display the value.
**Fix:** Resize the column width to fit the content.
- [`ISERR(ref)`](/functions/information/iserr), `TRUE` if `ref` is any error type except the `#N/A` error.
- [`ISERROR(ref)`](/functions/information/iserror), `TRUE` if `ref` is any error.
- [`ISNA(ref)`](/functions/information/isna), `TRUE` if ref is `#N/A`.
- [`ERROR.TYPE`](/functions/information/error.type) returns the numeric code for a given error.
- [`IFERROR(ref, value)`](/functions/logical/iferror) returns `value` if the content of `ref` is an error.
- [`IFNA(ref, value)`](/functions/logical/ifna) returns `value` only if the content of `ref` is the `#N/A` error.

View File

@@ -1,49 +0,0 @@
---
layout: doc
outline: deep
lang: en-US
---
# Optional Arguments
::: warning
**Note:** This draft page is under construction 🚧
:::
Any IronCalc function may accept zero, one or more arguments, which are values passed to the function when it is called from a spreadsheet formula.
Many function arguments are _required_. For such arguments, always pass a suitable value in the function call.
Some function arguments are _optional_. Optional arguments need not be passed in the function call and, in such cases, the function instead uses a predefined default value.
Consider a notional function called _FN\_NAME_, with the following syntax:
<p style="font-weight:bold;text-align:center;">FN_NAME(<span title="Number" style="color:#1E88E5">arg1</span>, <span title="Number" style="color:#1E88E5">arg2</span>, <span title="Number" style="color:#1E88E5">arg3</span>, <span title="Number" style="color:#1E88E5">arg4</span>=def1, <span title="Number" style="color:#1E88E5">arg5</span>=def2, <span title="Number" style="color:#1E88E5">arg6</span>=def3) => <span title="Number" style="color:#1E88E5" >fn_name</span></p>
Notes about this syntax:
* _FN_NAME_ is a function that takes six arguments (_arg1_, _arg2_, _arg3_, _arg4_, _arg5_ and _arg6_) and returns a value referred to as _fn_name_.
* For convenience in this case, all arguments and the returned value are colour-coded to indicate that they are numbers.
* Arguments _arg1_, _arg2_ and _arg3_ are <u>required</u> arguments and this would normally be stated in the **Argument descriptions** section of the function's description page.
* Arguments _arg4_, _arg5_ and _arg6_ are <u>optional</u> arguments and again this would normally be stated in the **Argument descriptions** section of the function's description page. In addition, optional arguments are usually indicated by the specification of a default value in the syntax.
* If _arg4_ is omitted, then the value _def1_ is assumed.
* If _arg5_ is omitted, then the value _def2_ is assumed.
* If _arg6_ is omitted, then the value _def3_ is assumed.
With this syntax, the following would all be valid calls to the _FN_NAME_ function:
**=FN\_NAME(1,2,3)**. All optional arguments omitted.
**=FN\_NAME(1,2,3,4)**. _arg4_ set to 4; optional arguments _arg5_ and _arg6_ assume default values.
**=FN\_NAME(1,2,3,,5)**. _arg5_ set to 5; optional arguments _arg4_ and _arg6_ assume default values.
**=FN\_NAME(1,2,3,,,6)**. _arg6_ set to 6; optional arguments _arg4_ and _arg5_ assume default values.
**=FN\_NAME(1,2,3,4,5)**. _arg4_ and _arg5_ set to 4 and 5 respectively; optional argument _arg6_ assumes default value.
**=FN\_NAME(1,2,3,4,,6)**. _arg4_ and _arg6_ set to 4 and 6 respectively; optional argument _arg5_ assumes default value.
**=FN\_NAME(1,2,3,,5,6)**. _arg5_ and _arg6_ set to 5 and 6 respectively; optional argument _arg4_ assumes default value.
**=FN\_NAME(1,2,3,4,5,6)**. Values passed for all optional arguments.

View File

@@ -1,13 +0,0 @@
---
layout: doc
outline: deep
lang: en-US
---
# Units
::: warning
**Note:** This draft page is under construction 🚧
:::
Some IronCalc functions return values that have units like currencies, percentage or dates.

View File

@@ -1,69 +0,0 @@
---
layout: doc
outline: deep
lang: en-US
---
# Value Types
::: warning
**Note:** This draft page is under construction 🚧
:::
In IronCalc a value, a result of a calculation, can be one of the following.
## Numbers
Numbers in IronCalc are [IEEE 754 double-precision](https://en.wikipedia.org/wiki/Double-precision_floating-point_format).
Numbers are only displayed up to 15 significant figures. That's why `=0.1+0.2` gives `0.3`.
Also, numbers are compared up to 15 significant figures. So `=IF(0.1+0.2=0.3, "Valid", "Invalid")` gives `Valid`.
However, `=0.3-0.2-0.1` will not give exactly `0` in IronCalc.
### Casting into numbers
Strings and booleans are sometimes converted to numbers:
`=1+"2"` => `3`
Some functions cast in weird ways:
`=SUM(1,TRUE)` => `1` and `=SUM(1,"1")` => `1`
And `=SUM(1,A1)` => `1` (where A1 contains `TRUE` or `"1"`)
Sometimes the conversion happens as might be expected. For example, `="123"+1` is `124`, `=SQRT("4")` is `2` and `=SQRT(TRUE)` is `1`.
Some functions, however, are more strict. For example, `=BIN2DEC(TRUE)` gives the #VALUE! error.
### Dates and times
IronCalc uses numbers to represent dates and times.
The integer part of the number represents the date, as a count of days since the fixed starting date of December 30, 1899.
The fractional part of the number represents the time of day. 0.0 corresponds to 00:00:00 (midnight) and 0.5 corresponds to 12:00:00 (noon).
## Strings
### Complex numbers
Using IronCalc, a complex number is a string of the form "1+j3".
## Booleans
### Casting from numbers
## Errors
### Casting from strings
"#N/A" => #N/A
## Arrays

View File

@@ -3,10 +3,8 @@ layout: doc
outline: deep outline: deep
lang: en-US lang: en-US
--- ---
# FV function # FV function
::: warning
**Note:** This draft page is under construction 🚧
:::
## Overview ## Overview
FV (<u>F</u>uture <u>V</u>alue) is a function of the Financial category that can be used to predict the future value of an investment or asset based on its present value. FV (<u>F</u>uture <u>V</u>alue) is a function of the Financial category that can be used to predict the future value of an investment or asset based on its present value.
@@ -15,38 +13,34 @@ FV can be used to calculate future value over a specified number of compounding
If your interest rate varies between periods, use the [FVSCHEDULE](/functions/financial/fvschedule) function instead of FV. If your interest rate varies between periods, use the [FVSCHEDULE](/functions/financial/fvschedule) function instead of FV.
## Usage ## Usage
### Syntax ### Syntax
**FV(<span title="Number" style="color:#1E88E5">rate</span>, <span title="Number" style="color:#1E88E5">nper</span>, <span title="Number" style="color:#1E88E5">pmt</span>, <span title="Number" style="color:#1E88E5">pv</span>=0, <span title="Boolean" style="color:#43A047">type</span>=FALSE) => <span title="Number" style="color:#1E88E5">fv</span>** **FV(rate, nper, pmt, pv, type)**
### Argument descriptions ### Argument descriptions
* *rate* ([number](/features/value-types#numbers), required). The fixed percentage interest rate or yield per period. * *rate*. The fixed percentage interest rate or yield per period.
* *nper* ([number](/features/value-types#numbers), required). "nper" stands for <u>n</u>umber of <u>per</u>iods, in this case the number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed. * *nper*. The number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed.
* *pmt* ([number](/features/value-types#numbers), required). "pmt" stands for <u>p</u>ay<u>m</u>en<u>t</u>, in this case the fixed amount paid or deposited each compounding period. * *pmt*. The fixed amount paid or deposited each compounding period.
* *pv* ([number](/features/value-types#numbers), [optional](/features/optional-arguments.md)). "pv" is the <u>p</u>resent <u>v</u>alue or starting amount of the asset (default 0). * *pv* (optional). The present value or starting amount of the asset (default 0).
* *type* ([boolean](/features/value-types/#booleans), [optional](/features/optional-arguments.md)). A logical value indicating whether the payment due dates are at the end (FALSE or 0) of the compounding periods or at the beginning (TRUE or any non-zero value). The default is FALSE when omitted. * *type* (optional). A logical value indicating whether the payment due dates are at the end (0) of the compounding periods or at the beginning (any non-zero value). The default is 0 when omitted.
### Additional guidance ### Additional guidance
* Make sure that the *rate* argument specifies the interest rate or yield applicable to the compounding period, based on the value chosen for *nper*. * Make sure that the *rate* argument specifies the interest rate or yield applicable to the compounding period, based on the value chosen for *nper*.
* The *pmt* and *pv* arguments should be expressed in the same currency unit. * The *pmt* and *pv* arguments should be expressed in the same currency unit. The value returned is expressed in the same currency unit.
* To ensure a worthwhile result, one of the *pmt* and *pv* arguments should be non-zero. * To ensure a worthwhile result, one of the *pmt* and *pv* arguments should be non-zero.
* The setting of the *type* argument only affects the calculation for non-zero values of the *pmt* argument. * The setting of the *type* argument only affects the calculation for non-zero values of the *pmt* argument.
### Returned value <!--@include: ../markdown-snippets/error-type-details.md-->
FV returns a [number](/features/value-types/#numbers) representing the future value expressed in the same [currency unit](/features/units) that was used for the *pmt* and *pv* arguments.
### Error conditions
* In common with many other IronCalc functions, FV propagates errors that are found in any of its arguments.
* If too few or too many arguments are supplied, FV returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of any of the *rate*, *nper*, *pmt* or *pv* arguments is not (or cannot be converted to) a [number](/features/value-types#value), then FV returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the value of the *type* argument is not (or cannot be converted to) a [Boolean](/features/value-types#booleans), then FV again returns the [`#VALUE!`](/features/error-types.md#value) error.
* For some combinations of valid argument values, FV may return a [`#NUM!`](/features/error-types.md#num) error or a [`#DIV/0!`](/features/error-types.md#div-0) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details ## Details
* If $\text{type} \neq 0$, $\text{fv}$ is given by the equation: * If *rate* = 0, FV is given by the equation:
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^\text{nper} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big) \times(1+\text{rate})}{\text{rate}}$$ $$
FV = -pv - (pmt \times nper)
$$
* If $\text{type} = 0$, $\text{fv}$ is given by the equation: * If *rate* <> 0 and *type* = 0, FV is given by the equation:
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^{\text{nper}} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big)}{\text{rate}}$$ $$ FV = -pv \times (1 + rate)^{nper} - \dfrac{pmt\times\big({(1+rate)^{nper}-1}\big)}{rate}
$$
* For any $\text{type}$, in the special case of $\text{rate} = 0$, $\text{fv}$ is given by the equation: * If *rate* <> 0 and *type* <> 0, FV is given by the equation:
$$ \text{fv} = -\text{pv} - (\text{pmt} \times \text{nper}) $$ $$ FV = -pv \times (1 + rate)^{nper} - \dfrac{pmt\times\big({(1+rate)^{nper}-1}\big) \times(1+rate)}{rate}
$$
## Examples ## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=fv). [See this example in IronCalc](https://app.ironcalc.com/?example=fv).
## Links ## Links
* For more information about the concept of "future value" in finance, visit Wikipedia's [Future value](https://en.wikipedia.org/wiki/Future_value) page. * For more information about the concept of "future value" in finance, visit Wikipedia's [Future value](https://en.wikipedia.org/wiki/Future_value) page.

View File

@@ -0,0 +1 @@
* For information about the different types of errors that you may encounter when using IronCalc functions, visit our [Error Types](/features/error-types) page.

View File

@@ -1 +0,0 @@
* For more information about the different types of errors that you may encounter when using IronCalc functions, visit our [Error Types](/features/error-types) page.

View File

@@ -1,185 +0,0 @@
import styled from "@emotion/styled";
import { Trash2 } from "lucide-react";
import { useEffect, useRef } from "react";
interface DeleteWorkbookDialogProperties {
onClose: () => void;
onConfirm: () => void;
workbookName: string;
}
function DeleteWorkbookDialog(properties: DeleteWorkbookDialogProperties) {
const deleteButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "blur(2px)";
}
if (deleteButtonRef.current) {
deleteButtonRef.current.focus();
}
return () => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "none";
}
};
}, []);
return (
<DialogWrapper
tabIndex={-1}
onKeyDown={(event) => {
if (event.code === "Escape") {
properties.onClose();
}
}}
role="dialog"
aria-labelledby="delete-dialog-title"
aria-describedby="delete-dialog-description"
>
<IconWrapper>
<Trash2 />
</IconWrapper>
<ContentWrapper>
<Title>Are you sure?</Title>
<Body>
The workbook <strong>'{properties.workbookName}'</strong> will be
permanently deleted. This action cannot be undone.
</Body>
<ButtonGroup>
<DeleteButton
onClick={() => {
properties.onConfirm();
properties.onClose();
}}
ref={deleteButtonRef}
>
Yes, delete workbook
</DeleteButton>
<CancelButton onClick={properties.onClose}>Cancel</CancelButton>
</ButtonGroup>
</ContentWrapper>
</DialogWrapper>
);
}
DeleteWorkbookDialog.displayName = "DeleteWorkbookDialog";
// some colors taken from the IronCalc palette
const COMMON_WHITE = "#FFF";
const COMMON_BLACK = "#272525";
const ERROR_MAIN = "#EB5757";
const ERROR_DARK = "#CB4C4C";
const GREY_200 = "#EEEEEE";
const GREY_300 = "#E0E0E0";
const GREY_700 = "#616161";
const GREY_900 = "#333333";
const PRIMARY_MAIN = "#F2994A";
const PRIMARY_DARK = "#D68742";
const DialogWrapper = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
display: flex;
flex-direction: column;
gap: 16px;
padding: 12px;
border-radius: 8px;
box-shadow: 0px 1px 3px 0px ${COMMON_BLACK}1A;
width: 280px;
max-width: calc(100% - 40px);
z-index: 50;
font-family: "Inter", sans-serif;
`;
const IconWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
border-radius: 4px;
background-color: ${ERROR_MAIN}1A;
margin: 12px auto 0 auto;
color: ${ERROR_MAIN};
svg {
width: 16px;
height: 16px;
}
`;
const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 14px;
word-break: break-word;
`;
const Title = styled.h2`
margin: 0;
font-weight: 600;
font-size: inherit;
color: ${GREY_900};
`;
const Body = styled.p`
margin: 0;
text-align: center;
color: ${GREY_900};
font-size: 12px;
`;
const ButtonGroup = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
width: 100%;
`;
const Button = styled.button`
cursor: pointer;
color: ${COMMON_WHITE};
background-color: ${PRIMARY_MAIN};
padding: 0px 10px;
height: 36px;
border-radius: 4px;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
text-overflow: ellipsis;
transition: background-color 150ms;
&:hover {
background-color: ${PRIMARY_DARK};
}
`;
const DeleteButton = styled(Button)`
background-color: ${ERROR_MAIN};
color: ${COMMON_WHITE};
&:hover {
background-color: ${ERROR_DARK};
}
`;
const CancelButton = styled(Button)`
background-color: ${GREY_200};
color: ${GREY_700};
&:hover {
background-color: ${GREY_300};
}
`;
export default DeleteWorkbookDialog;

View File

@@ -2,8 +2,7 @@ import styled from "@emotion/styled";
import { Menu, MenuItem, Modal } from "@mui/material"; import { Menu, MenuItem, Modal } from "@mui/material";
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react"; import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import DeleteWorkbookDialog from "./DeleteWorkbookDialog"; import { UploadFileDialog } from "./UploadFileDialog";
import UploadFileDialog from "./UploadFileDialog";
import { getModelsMetadata, getSelectedUuid } from "./storage"; import { getModelsMetadata, getSelectedUuid } from "./storage";
export function FileMenu(props: { export function FileMenu(props: {
@@ -19,7 +18,6 @@ export function FileMenu(props: {
const models = getModelsMetadata(); const models = getModelsMetadata();
const uuids = Object.keys(models); const uuids = Object.keys(models);
const selectedUuid = getSelectedUuid(); const selectedUuid = getSelectedUuid();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const elements = []; const elements = [];
for (const uuid of uuids) { for (const uuid of uuids) {
@@ -90,14 +88,16 @@ export function FileMenu(props: {
Download (.xlsx) Download (.xlsx)
</MenuItemText> </MenuItemText>
</MenuItemWrapper> </MenuItemWrapper>
<MenuItemWrapper <MenuItemWrapper>
onClick={() => {
setDeleteDialogOpen(true);
setMenuOpen(false);
}}
>
<StyledTrash /> <StyledTrash />
<MenuItemText>Delete workbook</MenuItemText> <MenuItemText
onClick={() => {
props.onDelete();
setMenuOpen(false);
}}
>
Delete workbook
</MenuItemText>
</MenuItemWrapper> </MenuItemWrapper>
<MenuDivider /> <MenuDivider />
{elements} {elements}
@@ -105,6 +105,10 @@ export function FileMenu(props: {
<Modal <Modal
open={isImportMenuOpen} open={isImportMenuOpen}
onClose={() => { onClose={() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "";
}
setImportMenuOpen(false); setImportMenuOpen(false);
}} }}
aria-labelledby="modal-modal-title" aria-labelledby="modal-modal-title"
@@ -113,26 +117,16 @@ export function FileMenu(props: {
<> <>
<UploadFileDialog <UploadFileDialog
onClose={() => { onClose={() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "";
}
setImportMenuOpen(false); setImportMenuOpen(false);
}} }}
onModelUpload={props.onModelUpload} onModelUpload={props.onModelUpload}
/> />
</> </>
</Modal> </Modal>
<Modal
open={isDeleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
aria-labelledby="delete-dialog-title"
aria-describedby="delete-dialog-description"
>
<>
<DeleteWorkbookDialog
onClose={() => setDeleteDialogOpen(false)}
onConfirm={props.onDelete}
workbookName={selectedUuid ? models[selectedUuid] : ""}
/>
</>
</Modal>
</> </>
); );
} }

View File

@@ -1,34 +1,17 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { BookOpen, FileUp, X } from "lucide-react"; import { BookOpen, FileUp } from "lucide-react";
import { type DragEvent, useEffect, useRef, useState } from "react"; import { type DragEvent, useRef, useState } from "react";
function UploadFileDialog(properties: { export function UploadFileDialog(properties: {
onClose: () => void; onClose: () => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>; onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
}) { }) {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const crossRef = useRef<HTMLDivElement>(null);
const { onModelUpload } = properties; const { onModelUpload } = properties;
useEffect(() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "blur(2px)";
}
if (crossRef.current) {
crossRef.current.focus();
}
return () => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "none";
}
};
}, []);
const handleClose = () => { const handleClose = () => {
properties.onClose(); properties.onClose();
}; };
@@ -96,16 +79,12 @@ function UploadFileDialog(properties: {
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
}; };
const root = document.getElementById("root");
if (root) {
root.style.filter = "blur(4px)";
}
return ( return (
<UploadDialog <UploadDialog>
tabIndex={-1}
role="dialog"
onKeyDown={(event) => {
if (event.code === "Escape") {
handleClose();
}
}}
>
<UploadTitle> <UploadTitle>
<span style={{ flexGrow: 2, marginLeft: 12 }}> <span style={{ flexGrow: 2, marginLeft: 12 }}>
Import an .xlsx file Import an .xlsx file
@@ -113,11 +92,29 @@ function UploadFileDialog(properties: {
<Cross <Cross
style={{ marginRight: 12 }} style={{ marginRight: 12 }}
onClick={handleClose} onClick={handleClose}
title="Close Dialog" onKeyDown={() => {}}
ref={crossRef}
tabIndex={0}
> >
<X /> <svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Close</title>
<path
d="M12 4.5L4 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 4.5L12 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Cross> </Cross>
</UploadTitle> </UploadTitle>
{message === "" ? ( {message === "" ? (
@@ -218,11 +215,6 @@ const Cross = styled("div")`
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
svg {
width: 16px;
height: 16px;
stroke-width: 1.5;
}
`; `;
const DocLink = styled("span")` const DocLink = styled("span")`
@@ -308,5 +300,3 @@ const DropZone = styled("div")`
gap: 16px; gap: 16px;
transition: 0.2s ease-in-out; transition: 0.2s ease-in-out;
`; `;
export default UploadFileDialog;

View File

@@ -1,4 +1,4 @@
import type { DefinedName, Model } from "@ironcalc/wasm"; import type { Model } from "@ironcalc/wasm";
import { import {
Box, Box,
Button, Button,
@@ -7,367 +7,91 @@ import {
DialogContent, DialogContent,
DialogTitle, DialogTitle,
IconButton, IconButton,
Stack,
styled,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Stack,
styled, styled,
} from "@mui/material"; } from "@mui/material";
import { t } from "i18next"; import { t } from "i18next";
import { BookOpen, Check, X } from "lucide-react"; import { BookOpen, X } from "lucide-react";
import { useEffect, useState } from "react"; import { useState } from "react";
import NamedRange from "./NamedRange"; import NamedRange from "./NamedRange";
import { getFullRangeToString } from "./util"; import type { NamedRangeObject } from "./NamedRange";
type NameManagerDialogProperties = { type NameManagerDialogProperties = {
onClose: () => void; onClose: () => void;
open: boolean; onSave: () => void;
model: Model;
onClose: () => void;
open: boolean; open: boolean;
model: Model; model: Model;
}; };
function NameManagerDialog(props: NameManagerDialogProperties) { function NameManagerDialog(props: NameManagerDialogProperties) {
const [definedNamesLocal, setDefinedNamesLocal] = useState<DefinedName[]>();
const [definedName, setDefinedName] = useState<DefinedName>({
name: "",
scope: undefined,
formula: "",
});
// render definedNames from model const handleClose = () => {
useEffect(() => {
if (props.open) {
const definedNamesModel = props.model.getDefinedNameList();
setDefinedNamesLocal(definedNamesModel);
console.log("definedNamesModel EFFECT", definedNamesModel);
}
}, [props.open]);
const handleSave = () => {
try {
console.log("SAVE", definedName);
props.model.newDefinedName(
definedName.name,
definedName.scope,
definedName.formula,
);
} catch (error) {
console.log("DefinedName save failed", error);
}
props.onClose(); props.onClose();
}; };
const handleChange = ( // update child component values in model
field: keyof DefinedName,
value: string | number | undefined,
) => {
setDefinedName((prev: DefinedName) => ({
...prev,
[field]: value,
}));
};
const handleDelete = (name: string, scope: number | undefined) => {
try {
props.model.deleteDefinedName(name, scope);
} catch (error) {
console.log("DefinedName delete failed", error);
}
// should re-render modal
};
const handleUpdate = (
name: string,
scope: number | undefined,
newName: string,
newScope: number | undefined,
newFormula: string,
) => {
try {
// partially update?
props.model.updateDefinedName(name, scope, newName, newScope, newFormula);
} catch (error) {
console.log("DefinedName update failed", error);
}
};
const formatFormula = (): string => {
const worksheets = props.model.getWorksheetsProperties();
const selectedView = props.model.getSelectedView();
const formatFormula = (): string => {
const worksheets = props.model.getWorksheetsProperties();
const selectedView = props.model.getSelectedView();
return getFullRangeToString(selectedView, worksheets);
};
return getFullRangeToString(selectedView, worksheets);
};
return (
<StyledDialog
open={props.open}
onClose={props.onClose}
maxWidth={false}
scroll="paper"
>
<StyledDialogTitle>
Named Ranges
<IconButton onClick={() => props.onClose()}>
<X size={16} />
</IconButton>
</StyledDialogTitle>
<StyledDialogContent dividers>
<StyledRangesHeader>
<Box width="171px">{t("name_manager_dialog.name")}</Box>
<Box width="171px">{t("name_manager_dialog.range")}</Box>
<Box width="171px">{t("name_manager_dialog.scope")}</Box>
</StyledRangesHeader>
{definedNamesLocal?.map((definedName) => (
<NamedRange
worksheets={props.model.getWorksheetsProperties()}
name={definedName.name}
scope={definedName.scope}
formula={definedName.formula}
key={definedName.name}
model={props.model}
onChange={handleChange}
onDelete={handleDelete}
canDelete={true}
/>
))}
<NamedRange
worksheets={props.model.getWorksheetsProperties()}
formula={formatFormula()}
onChange={handleChange}
canDelete={false}
model={props.model}
/>
</StyledDialogContent>
<StyledDialogActions>
<Box display="flex" alignItems="center" gap={"8px"}>
<BookOpen color="grey" size={16} />
<span style={{ fontSize: "12px", fontFamily: "Inter" }}>
{t("name_manager_dialog.help")}
</span>
</Box>
<Box display="flex" gap="8px" width={"155px"}>
{/* change hover color? */}
<Button
onClick={() => props.onClose()}
variant="contained"
disableElevation
color="info"
sx={{
bgcolor: (theme): string => theme.palette.grey["200"],
color: (theme): string => theme.palette.grey["700"],
textTransform: "none",
}}
>
Cancel
</Button>
<Button
onClick={handleSave}
variant="contained"
disableElevation
sx={{ textTransform: "none" }}
startIcon={<Check size={16} />}
// disabled={} // disable when error
>
Save
</Button>
</Box>
</StyledDialogActions>
</StyledDialog>
);
}
const StyledDialog = styled(Dialog)(() => ({
"& .MuiPaper-root": {
height: "380px",
minWidth: "620px",
},
}));
const StyledDialogTitle = styled(DialogTitle)`
padding: 12px 20px;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
`;
const StyledDialogContent = styled(DialogContent)`
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px 12px 20px 20px;
`;
const StyledRangesHeader = styled(Stack)(({ theme }) => ({
flexDirection: "row",
gap: "12px",
fontFamily: theme.typography.fontFamily,
fontSize: "12px",
fontWeight: "700",
color: theme.palette.info.main,
}));
const StyledDialogActions = styled(DialogActions)`
padding: 12px 20px;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #757575;
`;
export default NameManagerDialog;
import type { DefinedName, Model } from "@ironcalc/wasm";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Stack,
styled,
} from "@mui/material";
import { t } from "i18next";
import { BookOpen, Check, X } from "lucide-react";
import { useEffect, useState } from "react";
import NamedRange from "./NamedRange";
import { getShortRangeToString } from "./util";
type NameManagerDialogProperties = {
onClose: () => void;
open: boolean;
model: Model;
};
function NameManagerDialog(props: NameManagerDialogProperties) {
const [definedNamesLocal, setDefinedNamesLocal] = useState<DefinedName[]>();
const [definedName, setDefinedName] = useState<DefinedName>({
name: "",
scope: 0,
formula: "",
});
// render named ranges from model
useEffect(() => {
const definedNamesModel = props.model.getDefinedNameList();
setDefinedNamesLocal(definedNamesModel);
console.log("definedNamesModel EFFECT", definedNamesModel);
});
const handleSave = () => { const handleSave = () => {
console.log("SAVE DIALOG", definedName); props.onSave(); // => onNamedRangesUpdate from toolbar
// create newDefinedName and close props.onClose();
// props.model.newDefinedName( };
// definedName.name,
// definedName.scope, //! Why are fields editable only while clicking them?
// definedName.formula, // update child component values in UI
const handleChange = (id: string, field: string, value: string) => {
console.log("change:", id, field, value);
// previous array elements, plus updated value
// setNamedRangesLocal((prev) =>
// prev.map((namedRange) =>
// namedRange.id === id ? { ...namedRange, [field]: value } : namedRange,
// ),
// ); // );
props.onClose();
}; };
const handleChange = (field: keyof DefinedName, value: string | number) => { const nameList = props.model.getDefinedNameList();
setDefinedName((prev: DefinedName) => ({
...prev,
[field]: value,
}));
};
const handleDelete = () => {
console.log("definedName marked for deletion");
};
const formatFormula = (): string => {
const selectedView = props.model.getSelectedView();
return getShortRangeToString(selectedView);
};
return ( return (
<StyledDialog <StyledDialog
open={props.open} open={props.open}
onClose={props.onClose} onClose={props.onClose}
maxWidth={false} maxWidth="md"
fullWidth
scroll="paper" scroll="paper"
> >
<StyledDialogTitle> <StyledDialogTitle>
Named Ranges Named Ranges
<IconButton onClick={() => props.onClose()}> <IconButton onClick={handleClose}>
<X size={16} /> <X size={16} />
</IconButton> </IconButton>
</StyledDialogTitle> </StyledDialogTitle>
<StyledDialogContent dividers> <DialogContent dividers>
<StyledRangesHeader> {nameList.map((e) => (
<Box width="171px">{t("name_manager_dialog.name")}</Box>
<Box width="171px">{t("name_manager_dialog.range")}</Box>
<Box width="171px">{t("name_manager_dialog.scope")}</Box>
</StyledRangesHeader>
{definedNamesLocal?.map((definedName) => (
<NamedRange <NamedRange
worksheets={props.model.getWorksheetsProperties()} worksheets={props.model.getWorksheetsProperties()}
name={definedName.name} name={e.name}
scope={definedName.scope} scope={e.scope}
formula={definedName.formula} range={e.formula}
key={definedName.name} key={`${e.name}-${e.scope}`}
model={props.model}
onChange={handleChange} onChange={handleChange}
onDelete={handleDelete} model={props.model}
/> />
))} ))}
<NamedRange </DialogContent>
worksheets={props.model.getWorksheetsProperties()}
formula={formatFormula()}
onChange={handleChange}
model={props.model}
/>
</StyledDialogContent>
<StyledDialogActions> <StyledDialogActions>
<Box display="flex" alignItems="center" gap={"8px"}> <Box display="flex" alignItems="center">
<BookOpen color="grey" size={16} /> <BookOpen
color="grey"
style={{ width: 16, height: 16, marginLeft: 12, marginRight: 8 }}
/>
<span style={{ fontSize: "12px", fontFamily: "Inter" }}> <span style={{ fontSize: "12px", fontFamily: "Inter" }}>
{t("name_manager_dialog.help")} {t("name_manager_dialog.help")}
</span> </span>
</Box> </Box>
<Box display="flex" gap="8px" width={"155px"}> <Box display="flex" gap="10px">
{/* change hover color? */} <Button onClick={handleClose} variant="contained" color="info">
<Button
onClick={() => props.onClose()}
variant="contained"
disableElevation
color="info"
sx={{
bgcolor: (theme): string => theme.palette.grey["200"],
color: (theme): string => theme.palette.grey["700"],
textTransform: "none",
}}
>
Cancel Cancel
</Button> </Button>
<Button <Button onClick={handleSave} variant="contained">
onClick={handleSave} Save changes
variant="contained"
disableElevation
sx={{ textTransform: "none" }}
startIcon={<Check size={16} />}
>
Save
</Button> </Button>
</Box> </Box>
</StyledDialogActions> </StyledDialogActions>
@@ -377,17 +101,13 @@ function NameManagerDialog(props: NameManagerDialogProperties) {
const StyledDialog = styled(Dialog)(() => ({ const StyledDialog = styled(Dialog)(() => ({
"& .MuiPaper-root": { "& .MuiPaper-root": {
height: "380px", maxHeight: "60%",
minWidth: "620px", minHeight: "40%",
},
"& .MuiPaper-root": {
height: "380px",
minWidth: "620px",
}, },
})); }));
// font-weight: 600 is too bold compared to design, should be between 500 & 600
const StyledDialogTitle = styled(DialogTitle)` const StyledDialogTitle = styled(DialogTitle)`
padding: 12px 20px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
display: flex; display: flex;
@@ -395,30 +115,7 @@ align-items: center;
justify-content: space-between; justify-content: space-between;
`; `;
const StyledDialogContent = styled(DialogContent)`
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px 12px 20px 20px;
`;
const StyledRangesHeader = styled(Stack)(({ theme }) => ({
flexDirection: "row",
gap: "12px",
fontFamily: theme.typography.fontFamily,
fontSize: "12px",
fontWeight: "700",
color: theme.palette.info.main,
flexDirection: "row",
gap: "12px",
fontFamily: theme.typography.fontFamily,
fontSize: "12px",
fontWeight: "700",
color: theme.palette.info.main,
}));
const StyledDialogActions = styled(DialogActions)` const StyledDialogActions = styled(DialogActions)`
padding: 12px 20px;
height: 40px; height: 40px;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,168 +1,91 @@
import type { DefinedName, Model, WorksheetProperties } from "@ironcalc/wasm"; import type { Model, WorksheetProperties } from "@ironcalc/wasm";
import { import { Box, IconButton, MenuItem, TextField, styled } from "@mui/material";
Box,
Divider,
IconButton,
MenuItem,
TextField,
styled,
Box,
Divider,
IconButton,
MenuItem,
TextField,
styled,
} from "@mui/material";
import { Trash2 } from "lucide-react"; import { Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
export type NamedRangeObject = {
id: string;
name: string;
scope: string;
range: string;
};
type NamedRangeProperties = { type NamedRangeProperties = {
name?: string; name: string;
scope?: number; scope: string;
formula: string; range: string;
model: Model; model: Model;
worksheets: WorksheetProperties[]; worksheets: WorksheetProperties[];
onChange: ( // update namedRange in model
field: keyof DefinedName, onChange: (field: string, value: string) => void;
value: string | number | undefined,
) => void;
onDelete?: (name: string, scope: number | undefined) => void;
canDelete: boolean;
onUpdate?: (
name: string,
scope: number | undefined,
newName: string,
newScope: number | undefined,
newFormula: string,
) => void;
}; };
function NamedRange(props: NamedRangeProperties) { function NamedRange(props: NamedRangeProperties) {
const [name, setName] = useState(props.name || ""); // define onChange in parent for updating the model and values
const [scope, setScope] = useState(props.scope || undefined);
const [formula, setFormula] = useState(props.formula);
const [nameError, setNameError] = useState(false);
const [formulaError, setFormulaError] = useState(false);
const handleChange = (
field: keyof DefinedName,
value: string | number | undefined,
) => {
if (field === "name") {
setName(value as string);
props.onChange("name", value);
}
if (field === "scope") {
setScope(value as number | undefined);
props.onChange("scope", value);
}
if (field === "formula") {
setFormula(value as string);
props.onChange("formula", value);
}
};
useEffect(() => {
// send initial formula value to parent
handleChange("formula", formula);
}, []);
const handleDelete = () => { const handleDelete = () => {
props.onDelete?.(name, scope); // update model
console.log("deleted named range");
}; };
return ( return (
<> <StyledBox>
<StyledBox> <TextField
<StyledTextField variant="outlined"
id="name" size="small"
variant="outlined" margin="normal"
size="small" fullWidth
margin="none" value={props.name}
fullWidth onChange={(event) =>
error={nameError} props.onChange("name", event.target.value)
value={name} }
onChange={(event) => handleChange("name", event.target.value)} onKeyDown={(event) => {
onKeyDown={(event) => { event.stopPropagation();
event.stopPropagation(); }}
}} onClick={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()} />
/> <TextField
<StyledTextField variant="outlined"
id="scope" select
variant="outlined" defaultValue="Workbook"
select size="small"
size="small" margin="normal"
margin="none" fullWidth
fullWidth value={props.scope}
value={scope ?? "Workbook (Global)"} onChange={(event) =>
onChange={(event) => props.onChange("scope", event.target.value)
handleChange( }
"scope", >
event.target.value === "Workbook (Global)" {props.worksheets.map((option) => (
? undefined <MenuItem key={option.sheet_id} value={option.name}>
: event.target.value, {option.name}
) </MenuItem>
} ))}
> </TextField>
<MenuItem value="Workbook (Global)">Workbook (Global)</MenuItem> <TextField
{props.worksheets.map((option, index) => ( variant="outlined"
<MenuItem key={option.sheet_id} value={index}> size="small"
{option.name} margin="normal"
</MenuItem> fullWidth
))} value={props.range}
</StyledTextField> onChange={(event) =>
<StyledTextField props.onChange("range", event.target.value)
id="formula" }
variant="outlined" onKeyDown={(event) => {
size="small" event.stopPropagation();
margin="none" }}
fullWidth onClick={(event) => event.stopPropagation()}
error={formulaError} />
value={formula} {/* remove round hover animation */}
onChange={(event) => handleChange("formula", event.target.value)} <IconButton onClick={handleDelete}>
onKeyDown={(event) => { <Trash2 size={16} absoluteStrokeWidth />
event.stopPropagation(); </IconButton>
}} </StyledBox>
onClick={(event) => event.stopPropagation()}
/>
<StyledIconButton onClick={handleDelete} disabled={!props.canDelete}>
<Trash2 size={12} />
</StyledIconButton>
</StyledBox>
<Divider />
</>
); );
} }
const StyledBox = styled(Box)` const StyledBox = styled(Box)`
display: flex; display: flex;
gap: 12px; gap: 15px;
width: 577px;
`; `;
const StyledTextField = styled(TextField)(() => ({
"& .MuiInputBase-root": {
height: "28px",
margin: 0,
},
"& .MuiInputBase-root": {
height: "28px",
margin: 0,
},
}));
const StyledIconButton = styled(IconButton)(({ theme }) => ({
color: theme.palette.error.main,
"&.Mui-disabled": {
opacity: 0.6,
color: theme.palette.error.light,
},
color: theme.palette.error.main,
"&.Mui-disabled": {
opacity: 0.6,
color: theme.palette.error.light,
},
}));
export default NamedRange; export default NamedRange;

View File

@@ -59,10 +59,7 @@ const SheetListMenu = (properties: SheetListMenuProps) => {
)} )}
{hasColors && <ItemColor style={{ backgroundColor: tab.color }} />} {hasColors && <ItemColor style={{ backgroundColor: tab.color }} />}
<ItemName <ItemName
style={{ style={{ fontWeight: index === selectedIndex ? "bold" : "normal" }}
fontWeight: index === selectedIndex ? "bold" : "normal",
color: tab.state === "visible" ? "#333" : "#888",
}}
> >
{tab.name} {tab.name}
</ItemName> </ItemName>

View File

@@ -1,5 +1,4 @@
import { Dialog, TextField, styled } from "@mui/material"; import { Dialog, TextField, styled } from "@mui/material";
import { Check, X } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { theme } from "../../theme"; import { theme } from "../../theme";
@@ -21,13 +20,28 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
<Dialog open={properties.open} onClose={properties.onClose}> <Dialog open={properties.open} onClose={properties.onClose}>
<StyledDialogTitle> <StyledDialogTitle>
{t("sheet_rename.title")} {t("sheet_rename.title")}
<Cross <Cross onClick={handleClose} onKeyDown={() => {}}>
onClick={handleClose} <svg
title={t("sheet_rename.close")} width="16"
tabIndex={-1} height="16"
onKeyDown={() => {}} viewBox="0 0 16 16"
> fill="none"
<X /> xmlns="http://www.w3.org/2000/svg"
>
<title>Close</title>
<path
d="M12 4.5L4 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 4.5L12 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Cross> </Cross>
</StyledDialogTitle> </StyledDialogTitle>
<StyledDialogContent> <StyledDialogContent>
@@ -40,8 +54,6 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
if (event.key === "Enter") { if (event.key === "Enter") {
properties.onNameChanged(name); properties.onNameChanged(name);
properties.onClose(); properties.onClose();
} else if (event.key === "Escape") {
properties.onClose();
} }
}} }}
onChange={(event) => { onChange={(event) => {
@@ -49,8 +61,6 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
}} }}
spellCheck="false" spellCheck="false"
onPaste={(event) => event.stopPropagation()} onPaste={(event) => event.stopPropagation()}
onCopy={(event) => event.stopPropagation()}
onCut={(event) => event.stopPropagation()}
/> />
</StyledDialogContent> </StyledDialogContent>
<DialogFooter> <DialogFooter>
@@ -59,9 +69,6 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
properties.onNameChanged(name); properties.onNameChanged(name);
}} }}
> >
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
{t("sheet_rename.rename")} {t("sheet_rename.rename")}
</StyledButton> </StyledButton>
</DialogFooter> </DialogFooter>
@@ -83,7 +90,7 @@ const StyledDialogTitle = styled("div")`
const Cross = styled("div")` const Cross = styled("div")`
&:hover { &:hover {
background-color: ${theme.palette.grey["50"]}; background-color: ${theme.palette.grey["100"]};
} }
display: flex; display: flex;
border-radius: 4px; border-radius: 4px;
@@ -92,11 +99,6 @@ const Cross = styled("div")`
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
svg {
width: 16px;
height: 16px;
stroke-width: 1.5;
}
`; `;
const StyledDialogContent = styled("div")` const StyledDialogContent = styled("div")`

View File

@@ -1,5 +1,4 @@
import { Button, Menu, MenuItem, styled } from "@mui/material"; import { Button, Menu, MenuItem, styled } from "@mui/material";
import type { MenuItemProps } from "@mui/material";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { theme } from "../../theme"; import { theme } from "../../theme";
@@ -15,9 +14,7 @@ interface SheetTabProps {
onSelected: () => void; onSelected: () => void;
onColorChanged: (hex: string) => void; onColorChanged: (hex: string) => void;
onRenamed: (name: string) => void; onRenamed: (name: string) => void;
canDelete: boolean;
onDeleted: () => void; onDeleted: () => void;
onHideSheet: () => void;
workbookState: WorkbookState; workbookState: WorkbookState;
} }
@@ -95,23 +92,14 @@ function SheetTab(props: SheetTabProps) {
Change Color Change Color
</StyledMenuItem> </StyledMenuItem>
<StyledMenuItem <StyledMenuItem
disabled={!props.canDelete}
onClick={() => { onClick={() => {
props.onDeleted(); props.onDeleted();
handleClose(); handleClose();
}} }}
> >
{" "}
Delete Delete
</StyledMenuItem> </StyledMenuItem>
<StyledMenuItem
disabled={!props.canDelete}
onClick={() => {
props.onHideSheet();
handleClose();
}}
>
Hide sheet
</StyledMenuItem>
</StyledMenu> </StyledMenu>
<SheetRenameDialog <SheetRenameDialog
open={renameDialogOpen} open={renameDialogOpen}
@@ -149,19 +137,16 @@ const StyledMenu = styled(Menu)`
} }
`; `;
const StyledMenuItem = styled(MenuItem)<MenuItemProps>(() => ({ const StyledMenuItem = styled(MenuItem)`
display: "flex", display: flex;
justifyContent: "space-between", justify-content: space-between;
fontSize: "12px", font-size: 12px;
width: "calc(100% - 8px)", width: calc(100% - 8px);
margin: "0px 4px", margin: 0px 4px;
borderRadius: "4px", border-radius: 4px;
padding: "8px", padding: 8px;
height: "32px", height: 32px;
"&:disabled": { `;
color: "#BDBDBD",
},
}));
const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>` const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>`
display: flex; display: flex;

View File

@@ -19,7 +19,6 @@ export interface SheetTabBarProps {
onSheetColorChanged: (hex: string) => void; onSheetColorChanged: (hex: string) => void;
onSheetRenamed: (name: string) => void; onSheetRenamed: (name: string) => void;
onSheetDeleted: () => void; onSheetDeleted: () => void;
onHideSheet: () => void;
} }
function SheetTabBar(props: SheetTabBarProps) { function SheetTabBar(props: SheetTabBarProps) {
@@ -34,18 +33,6 @@ function SheetTabBar(props: SheetTabBarProps) {
setAnchorEl(null); setAnchorEl(null);
}; };
const nonHidenSheets = sheets
.map((s, index) => {
return {
state: s.state,
index,
name: s.name,
color: s.color,
sheetId: s.sheetId,
};
})
.filter((s) => s.state === "visible");
return ( return (
<Container> <Container>
<LeftButtonsContainer> <LeftButtonsContainer>
@@ -67,24 +54,22 @@ function SheetTabBar(props: SheetTabBarProps) {
<VerticalDivider /> <VerticalDivider />
<Sheets> <Sheets>
<SheetInner> <SheetInner>
{nonHidenSheets.map((tab) => ( {sheets.map((tab, index) => (
<SheetTab <SheetTab
key={tab.sheetId} key={tab.sheetId}
name={tab.name} name={tab.name}
color={tab.color} color={tab.color}
selected={tab.index === selectedIndex} selected={index === selectedIndex}
onSelected={() => onSheetSelected(tab.index)} onSelected={() => onSheetSelected(index)}
onColorChanged={(hex: string): void => { onColorChanged={(hex: string): void => {
props.onSheetColorChanged(hex); props.onSheetColorChanged(hex);
}} }}
onRenamed={(name: string): void => { onRenamed={(name: string): void => {
props.onSheetRenamed(name); props.onSheetRenamed(name);
}} }}
canDelete={nonHidenSheets.length > 1}
onDeleted={(): void => { onDeleted={(): void => {
props.onSheetDeleted(); props.onSheetDeleted();
}} }}
onHideSheet={props.onHideSheet}
workbookState={workbookState} workbookState={workbookState}
/> />
))} ))}

View File

@@ -2,5 +2,4 @@ export interface SheetOptions {
name: string; name: string;
color: string; color: string;
sheetId: number; sheetId: number;
state: string;
} }

View File

@@ -5,10 +5,10 @@ export const headerGlobalSelectorColor = "#EAECF4";
export const headerSelectedBackground = "#EEEEEE"; export const headerSelectedBackground = "#EEEEEE";
export const headerFullSelectedBackground = "#D3D6E9"; export const headerFullSelectedBackground = "#D3D6E9";
export const headerSelectedColor = "#333"; export const headerSelectedColor = "#333";
export const headerBorderColor = "#E0E0E0"; export const headerBorderColor = "#DEE0EF";
export const gridColor = "#E0E0E0"; export const gridColor = "#E0E0E0";
export const gridSeparatorColor = "#E0E0E0"; export const gridSeparatorColor = "#D3D6E9";
export const defaultTextColor = "#2E414D"; export const defaultTextColor = "#2E414D";
export const outlineColor = "#F2994A"; export const outlineColor = "#F2994A";

View File

@@ -45,8 +45,8 @@ const ColorPicker = (properties: ColorPickerProps) => {
"#3BB68A", "#3BB68A",
"#8CB354", "#8CB354",
"#F8CD3C", "#F8CD3C",
"#F2994A",
"#EC5753", "#EC5753",
"#A23C52",
"#D03627", "#D03627",
"#523E93", "#523E93",
"#3358B7", "#3358B7",

View File

@@ -1,6 +1,6 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { Dialog, TextField } from "@mui/material"; import { Dialog, TextField } from "@mui/material";
import { Check, X } from "lucide-react"; import { Check } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { theme } from "../theme"; import { theme } from "../theme";
@@ -36,8 +36,28 @@ const FormatPicker = (properties: FormatPickerProps) => {
> >
<StyledDialogTitle> <StyledDialogTitle>
{t("num_fmt.title")} {t("num_fmt.title")}
<Cross onClick={handleClose} title={t("num_fmt.close")}> <Cross onClick={handleClose} onKeyDown={() => {}}>
<X /> <svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Close</title>
<path
d="M12 4.5L4 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 4.5L12 12.5"
stroke="#333333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Cross> </Cross>
</StyledDialogTitle> </StyledDialogTitle>
@@ -81,7 +101,7 @@ const StyledDialogTitle = styled("div")`
const Cross = styled("div")` const Cross = styled("div")`
&:hover { &:hover {
background-color: ${theme.palette.grey["50"]}; background-color: ${theme.palette.grey["100"]};
} }
display: flex; display: flex;
border-radius: 4px; border-radius: 4px;
@@ -90,11 +110,6 @@ const Cross = styled("div")`
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
svg {
width: 16px;
height: 16px;
stroke-width: 1.5;
}
`; `;
const StyledDialogContent = styled("div")` const StyledDialogContent = styled("div")`

View File

@@ -37,7 +37,6 @@ import {
} from "../icons"; } from "../icons";
import { theme } from "../theme"; import { theme } from "../theme";
import NameManagerDialog from "./NameManagerDialog"; import NameManagerDialog from "./NameManagerDialog";
import NameManagerDialog from "./NameManagerDialog";
import BorderPicker from "./borderPicker"; import BorderPicker from "./borderPicker";
import ColorPicker from "./colorPicker"; import ColorPicker from "./colorPicker";
import { TOOLBAR_HEIGHT } from "./constants"; import { TOOLBAR_HEIGHT } from "./constants";
@@ -64,6 +63,7 @@ type ToolbarProperties = {
onFillColorPicked: (hex: string) => void; onFillColorPicked: (hex: string) => void;
onNumberFormatPicked: (numberFmt: string) => void; onNumberFormatPicked: (numberFmt: string) => void;
onBorderChanged: (border: BorderOptions) => void; onBorderChanged: (border: BorderOptions) => void;
onNamedRangesUpdate: () => void;
fillColor: string; fillColor: string;
fontColor: string; fontColor: string;
bold: boolean; bold: boolean;
@@ -398,6 +398,11 @@ function Toolbar(properties: ToolbarProperties) {
onClose={() => { onClose={() => {
setNameManagerDialogOpen(false); setNameManagerDialogOpen(false);
}} }}
onSave={() => {
console.log(
"update NamedRanges in model => properties.onNamedRangesUpdate",
);
}}
model={properties.model} model={properties.model}
/> />
</ToolbarContainer> </ToolbarContainer>

View File

@@ -1,10 +1,6 @@
import type { Area, Cell } from "./types"; import type { Area, Cell } from "./types";
import { import { columnNameFromNumber } from "@ironcalc/wasm";
type SelectedView,
type WorksheetProperties,
columnNameFromNumber,
} from "@ironcalc/wasm";
/** /**
* Returns true if the keypress should start editing * Returns true if the keypress should start editing
@@ -61,24 +57,7 @@ export function rangeToStr(
if (rowStart === rowEnd && columnStart === columnEnd) { if (rowStart === rowEnd && columnStart === columnEnd) {
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`; return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`;
} }
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}:${columnNameFromNumber( return `${sheetName}${columnNameFromNumber(
columnEnd, columnStart,
)}${rowEnd}`; )}${rowStart}:${columnNameFromNumber(columnEnd)}${rowEnd}`;
}
export function getFullRangeToString(
selectedView: SelectedView,
worksheets: WorksheetProperties[], // solo pasar names
): string {
// order of values is confusing compared to rangeToStr range type, needs refactoring for consistency
const [rowStart, columnStart, rowEnd, columnEnd] = selectedView.range;
const sheetNames = worksheets.map((s) => s.name);
const sheetName = `${sheetNames[selectedView.sheet]}!`;
if (rowStart === rowEnd && columnStart === columnEnd) {
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`;
}
return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}:${columnNameFromNumber(
columnEnd,
)}${rowEnd}`;
} }

View File

@@ -5,9 +5,7 @@ import type {
WorksheetProperties, WorksheetProperties,
} from "@ironcalc/wasm"; } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { PaintRoller } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import ReactDOMServer from "react-dom/server";
import SheetTabBar from "./SheetTabBar/SheetTabBar"; import SheetTabBar from "./SheetTabBar/SheetTabBar";
import { import {
COLUMN_WIDTH_SCALE, COLUMN_WIDTH_SCALE,
@@ -34,8 +32,8 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const setRedrawId = useState(0)[1]; const setRedrawId = useState(0)[1];
const info = model const info = model
.getWorksheetsProperties() .getWorksheetsProperties()
.map(({ name, color, sheet_id, state }: WorksheetProperties) => { .map(({ name, color, sheet_id }: WorksheetProperties) => {
return { name, color: color ? color : "#FFF", sheetId: sheet_id, state }; return { name, color: color ? color : "#FFF", sheetId: sheet_id };
}); });
const focusWorkbook = useCallback(() => { const focusWorkbook = useCallback(() => {
if (rootRef.current) { if (rootRef.current) {
@@ -117,7 +115,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const onNamedRangesUpdate = () => { const onNamedRangesUpdate = () => {
// update named ranges in model // update named ranges in model
}; }
const onCopyStyles = () => { const onCopyStyles = () => {
const { const {
@@ -142,15 +140,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const el = rootRef.current?.getElementsByClassName("sheet-container")[0]; const el = rootRef.current?.getElementsByClassName("sheet-container")[0];
if (el) { if (el) {
(el as HTMLElement).style.cursor = (el as HTMLElement).style.cursor =
`url('data:image/svg+xml;utf8,${encodeURIComponent( `url('data:image/svg+xml;utf8,<svg data-v-56bd7dfc="" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paintbrush-vertical"><path d="M10 2v2"></path><path d="M14 2v4"></path><path d="M17 2a1 1 0 0 1 1 1v9H6V3a1 1 0 0 1 1-1z"></path><path d="M6 12a1 1 0 0 0-1 1v1a2 2 0 0 0 2 2h2a1 1 0 0 1 1 1v2.9a2 2 0 1 0 4 0V17a1 1 0 0 1 1-1h2a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1"></path></svg>'), auto`;
ReactDOMServer.renderToString(
<PaintRoller
width={24}
height={24}
style={{ transform: "rotate(-8deg)" }}
/>,
),
)}'), auto`;
} }
}; };
@@ -573,6 +563,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
model.setShowGridLines(sheet, show); model.setShowGridLines(sheet, show);
setRedrawId((id) => id + 1); setRedrawId((id) => id + 1);
}} }}
onNamedRangesUpdate={onNamedRangesUpdate}
model={model} model={model}
/> />
<FormulaBar <FormulaBar
@@ -601,9 +592,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
selectedIndex={model.getSelectedSheet()} selectedIndex={model.getSelectedSheet()}
workbookState={workbookState} workbookState={workbookState}
onSheetSelected={(sheet: number): void => { onSheetSelected={(sheet: number): void => {
if (info[sheet].state !== "visible") {
model.unhideSheet(sheet);
}
model.setSelectedSheet(sheet); model.setSelectedSheet(sheet);
setRedrawId((value) => value + 1); setRedrawId((value) => value + 1);
}} }}
@@ -634,11 +622,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
model.deleteSheet(selectedSheet); model.deleteSheet(selectedSheet);
setRedrawId((value) => value + 1); setRedrawId((value) => value + 1);
}} }}
onHideSheet={(): void => {
const selectedSheet = model.getSelectedSheet();
model.hideSheet(selectedSheet);
setRedrawId((value) => value + 1);
}}
/> />
</Container> </Container>
); );

View File

@@ -19,7 +19,6 @@
"decimal_places_decrease": "Decrease decimal places", "decimal_places_decrease": "Decrease decimal places",
"show_hide_grid_lines": "Show/hide grid lines", "show_hide_grid_lines": "Show/hide grid lines",
"name_manager": "Name manager", "name_manager": "Name manager",
"name_manager": "Name manager",
"vertical_align_bottom": "Align bottom", "vertical_align_bottom": "Align bottom",
"vertical_align_middle": " Align middle", "vertical_align_middle": " Align middle",
"vertical_align_top": "Align top", "vertical_align_top": "Align top",
@@ -60,14 +59,12 @@
"num_fmt": { "num_fmt": {
"title": "Custom number format", "title": "Custom number format",
"label": "Number format", "label": "Number format",
"close": "Close dialog",
"save": "Save" "save": "Save"
}, },
"sheet_rename": { "sheet_rename": {
"rename": "Save", "rename": "Save",
"label": "New name", "label": "New name",
"title": "Rename Sheet", "title": "Rename Sheet"
"close": "Close dialog"
}, },
"formula_input": { "formula_input": {
"update": "Update", "update": "Update",
@@ -79,15 +76,6 @@
"sheet_list": "Sheet list" "sheet_list": "Sheet list"
}, },
"name_manager_dialog": { "name_manager_dialog": {
"help": "Learn more about Named Ranges", "help": "Learn more about Named Ranges"
"name": "Name",
"range": "Scope",
"scope": "Range"
},
"name_manager_dialog": {
"help": "Learn more about Named Ranges",
"name": "Name",
"range": "Scope",
"scope": "Range"
} }
} }

View File

@@ -41,30 +41,6 @@ pub(crate) struct Relationship {
pub(crate) rel_type: String, pub(crate) rel_type: String,
} }
impl WorkbookXML {
fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>)> {
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
let defined_names = self
.defined_names
.iter()
.map(|dn| {
let index = dn
.sheet_id
.and_then(|sheet_id| {
// returns an Option<usize>
sheet_id_index.iter().position(|&x| x == sheet_id)
})
// convert Option<usize> to Option<u32>
.map(|pos| pos as u32);
(dn.name.clone(), index)
})
.collect::<Vec<_>>();
defined_names
}
}
fn get_column_from_ref(s: &str) -> String { fn get_column_from_ref(s: &str) -> String {
let cs = s.chars(); let cs = s.chars();
let mut column = Vec::<char>::new(); let mut column = Vec::<char>::new();
@@ -304,12 +280,11 @@ fn from_a1_to_rc(
worksheets: &[String], worksheets: &[String],
context: String, context: String,
tables: HashMap<String, Table>, tables: HashMap<String, Table>,
defined_names: Vec<(String, Option<u32>)>,
) -> Result<String, XlsxError> { ) -> Result<String, XlsxError> {
let mut parser = Parser::new(worksheets.to_owned(), defined_names, tables); let mut parser = Parser::new(worksheets.to_owned(), tables);
let cell_reference = let cell_reference =
parse_reference(&context).map_err(|error| XlsxError::Xml(error.to_string()))?; parse_reference(&context).map_err(|error| XlsxError::Xml(error.to_string()))?;
let t = parser.parse(&formula, &cell_reference); let t = parser.parse(&formula, &Some(cell_reference));
Ok(to_rc_format(&t)) Ok(to_rc_format(&t))
} }
@@ -706,7 +681,6 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
worksheets: &[String], worksheets: &[String],
tables: &HashMap<String, Table>, tables: &HashMap<String, Table>,
shared_strings: &mut Vec<String>, shared_strings: &mut Vec<String>,
defined_names: Vec<(String, Option<u32>)>,
) -> Result<(Worksheet, bool), XlsxError> { ) -> Result<(Worksheet, bool), XlsxError> {
let sheet_name = &settings.name; let sheet_name = &settings.name;
let sheet_id = settings.id; let sheet_id = settings.id;
@@ -881,13 +855,8 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
// It's the mother cell. We do not use the ref attribute in IronCalc // It's the mother cell. We do not use the ref attribute in IronCalc
let formula = fs[0].text().unwrap_or("").to_string(); let formula = fs[0].text().unwrap_or("").to_string();
let context = format!("{}!{}", sheet_name, cell_ref); let context = format!("{}!{}", sheet_name, cell_ref);
let formula = from_a1_to_rc( let formula =
formula, from_a1_to_rc(formula, worksheets, context, tables.clone())?;
worksheets,
context,
tables.clone(),
defined_names.clone(),
)?;
match index_map.get(&si) { match index_map.get(&si) {
Some(index) => { Some(index) => {
// The index for that formula already exists meaning we bumped into a daughter cell first // The index for that formula already exists meaning we bumped into a daughter cell first
@@ -941,13 +910,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
// Its a cell with a simple formula // Its a cell with a simple formula
let formula = fs[0].text().unwrap_or("").to_string(); let formula = fs[0].text().unwrap_or("").to_string();
let context = format!("{}!{}", sheet_name, cell_ref); let context = format!("{}!{}", sheet_name, cell_ref);
let formula = from_a1_to_rc( let formula = from_a1_to_rc(formula, worksheets, context, tables.clone())?;
formula,
worksheets,
context,
tables.clone(),
defined_names.clone(),
)?;
match get_formula_index(&formula, &shared_formulas) { match get_formula_index(&formula, &shared_formulas) {
Some(index) => formula_index = index, Some(index) => formula_index = index,
@@ -1060,9 +1023,6 @@ pub(super) fn load_sheets<R: Read + std::io::Seek>(
let mut sheets = Vec::new(); let mut sheets = Vec::new();
let mut selected_sheet = 0; let mut selected_sheet = 0;
let mut sheet_index = 0; let mut sheet_index = 0;
let defined_names = workbook.get_defined_names_with_scope();
for sheet in &workbook.worksheets { for sheet in &workbook.worksheets {
let sheet_name = &sheet.name; let sheet_name = &sheet.name;
let rel_id = &sheet.id; let rel_id = &sheet.id;
@@ -1084,15 +1044,8 @@ pub(super) fn load_sheets<R: Read + std::io::Seek>(
.ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))? .ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))?
.to_vec(), .to_vec(),
}; };
let (s, is_selected) = load_sheet( let (s, is_selected) =
archive, load_sheet(archive, &path, settings, worksheets, tables, shared_strings)?;
&path,
settings,
worksheets,
tables,
shared_strings,
defined_names.clone(),
)?;
if is_selected { if is_selected {
selected_sheet = sheet_index; selected_sheet = sheet_index;
} }

Binary file not shown.