From a39019b0f2d77d2355870c57448cee2a50506164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Wed, 20 Nov 2024 00:35:19 +0100 Subject: [PATCH] UPDATE: Adds implicit intersection operator! --- base/src/actions.rs | 52 +- base/src/cast.rs | 55 +- base/src/diffs.rs | 138 --- base/src/expressions/lexer/mod.rs | 1 + base/src/expressions/lexer/test/mod.rs | 1 + .../lexer/test/test_implicit_intersection.rs | 25 + base/src/expressions/parser/mod.rs | 60 +- base/src/expressions/parser/move_formula.rs | 8 +- .../src/expressions/parser/static_analysis.rs | 984 ++++++++++++++++++ base/src/expressions/parser/stringify.rs | 125 ++- base/src/expressions/parser/tests/mod.rs | 1 + .../tests/test_add_implicit_intersection.rs | 80 ++ .../tests/test_implicit_intersection.rs | 38 + .../parser/tests/test_move_formula.rs | 78 +- .../expressions/parser/tests/test_tables.rs | 5 +- base/src/expressions/parser/walk.rs | 278 ----- base/src/expressions/token.rs | 1 + base/src/functions/information.rs | 2 +- base/src/functions/lookup_and_reference.rs | 2 +- base/src/lib.rs | 1 - base/src/model.rs | 74 +- base/src/new_empty.rs | 7 +- base/src/test/mod.rs | 2 +- base/src/test/test_fn_concatenate.rs | 5 +- base/src/test/test_fn_formulatext.rs | 14 +- base/src/test/test_forward_references.rs | 121 --- base/src/test/test_implicit_intersection.rs | 50 + base/src/units.rs | 1 + base/src/workbook.rs | 4 +- xlsx/src/import/worksheets.rs | 15 +- xlsx/tests/test.rs | 33 +- 31 files changed, 1569 insertions(+), 692 deletions(-) delete mode 100644 base/src/diffs.rs create mode 100644 base/src/expressions/lexer/test/test_implicit_intersection.rs create mode 100644 base/src/expressions/parser/static_analysis.rs create mode 100644 base/src/expressions/parser/tests/test_add_implicit_intersection.rs create mode 100644 base/src/expressions/parser/tests/test_implicit_intersection.rs delete mode 100644 base/src/expressions/parser/walk.rs delete mode 100644 base/src/test/test_forward_references.rs create mode 100644 base/src/test/test_implicit_intersection.rs diff --git a/base/src/actions.rs b/base/src/actions.rs index 126466f9..1fec0008 100644 --- a/base/src/actions.rs +++ b/base/src/actions.rs @@ -1,5 +1,6 @@ use crate::constants::{LAST_COLUMN, LAST_ROW}; -use crate::expressions::parser::stringify::DisplaceData; +use crate::expressions::parser::stringify::{to_string, to_string_displaced, DisplaceData}; +use crate::expressions::types::CellReferenceRC; use crate::model::Model; // NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns @@ -8,16 +9,45 @@ use crate::model::Model; // I feel this is unimportant for now. impl Model { + fn shift_cell_formula( + &mut self, + sheet: u32, + row: i32, + column: i32, + displace_data: &DisplaceData, + ) -> Result<(), String> { + if let Some(f) = self + .workbook + .worksheet(sheet)? + .cell(row, column) + .and_then(|c| c.get_formula()) + { + let node = &self.parsed_formulas[sheet as usize][f as usize].clone(); + let cell_reference = CellReferenceRC { + sheet: self.workbook.worksheets[sheet as usize].get_name(), + row, + column, + }; + // FIXME: This is not a very performant way if the formula has changed :S. + let formula = to_string(node, &cell_reference); + let formula_displaced = to_string_displaced(node, &cell_reference, displace_data); + if formula != formula_displaced { + self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))?; + } + } + Ok(()) + } /// This function iterates over all cells in the model and shifts their formulas according to the displacement data. /// /// # Arguments /// /// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude. - fn displace_cells(&mut self, displace_data: &DisplaceData) { + fn displace_cells(&mut self, displace_data: &DisplaceData) -> Result<(), String> { let cells = self.get_all_cells(); for cell in cells { - self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data); + self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data)?; } + Ok(()) } /// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order. @@ -134,7 +164,7 @@ impl Model { column, delta: column_count, }), - ); + )?; Ok(()) } @@ -187,7 +217,7 @@ impl Model { column, delta: -column_count, }), - ); + )?; let worksheet = &mut self.workbook.worksheet_mut(sheet)?; // deletes all the column styles @@ -311,7 +341,7 @@ impl Model { row, delta: row_count, }), - ); + )?; Ok(()) } @@ -372,7 +402,7 @@ impl Model { row, delta: -row_count, }), - ); + )?; Ok(()) } @@ -393,14 +423,14 @@ impl Model { sheet: u32, column: i32, delta: i32, - ) -> Result<(), &'static str> { + ) -> Result<(), String> { // Check boundaries let target_column = column + delta; if !(1..=LAST_COLUMN).contains(&target_column) { - return Err("Target column out of boundaries"); + return Err("Target column out of boundaries".to_string()); } if !(1..=LAST_COLUMN).contains(&column) { - return Err("Initial column out of boundaries"); + return Err("Initial column out of boundaries".to_string()); } // TODO: Add the actual displacement of data and styles @@ -412,7 +442,7 @@ impl Model { column, delta, }), - ); + )?; Ok(()) } diff --git a/base/src/cast.rs b/base/src/cast.rs index 9d0e2e46..afc6338f 100644 --- a/base/src/cast.rs +++ b/base/src/cast.rs @@ -1,7 +1,6 @@ use crate::{ calc_result::{CalcResult, Range}, expressions::{parser::Node, token::Error, types::CellReferenceIndex}, - implicit_intersection::implicit_intersection, model::Model, }; @@ -39,19 +38,11 @@ impl Model { } CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0), error @ CalcResult::Error { .. } => Err(error), - CalcResult::Range { left, right } => { - match implicit_intersection(&cell, &Range { left, right }) { - Some(cell_reference) => { - let result = self.evaluate_cell(cell_reference); - self.cast_to_number(result, cell_reference) - } - None => Err(CalcResult::Error { - error: Error::VALUE, - origin: cell, - message: "Invalid reference (number)".to_string(), - }), - } - } + CalcResult::Range { .. } => Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }), } } @@ -99,19 +90,11 @@ impl Model { } CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()), error @ CalcResult::Error { .. } => Err(error), - CalcResult::Range { left, right } => { - match implicit_intersection(&cell, &Range { left, right }) { - Some(cell_reference) => { - let result = self.evaluate_cell(cell_reference); - self.cast_to_string(result, cell_reference) - } - None => Err(CalcResult::Error { - error: Error::VALUE, - origin: cell, - message: "Invalid reference (string)".to_string(), - }), - } - } + CalcResult::Range { .. } => Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }), } } @@ -151,19 +134,11 @@ impl Model { CalcResult::Boolean(b) => Ok(b), CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false), error @ CalcResult::Error { .. } => Err(error), - CalcResult::Range { left, right } => { - match implicit_intersection(&cell, &Range { left, right }) { - Some(cell_reference) => { - let result = self.evaluate_cell(cell_reference); - self.cast_to_bool(result, cell_reference) - } - None => Err(CalcResult::Error { - error: Error::VALUE, - origin: cell, - message: "Invalid reference (bool)".to_string(), - }), - } - } + CalcResult::Range { .. } => Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }), } } diff --git a/base/src/diffs.rs b/base/src/diffs.rs deleted file mode 100644 index e02c1153..00000000 --- a/base/src/diffs.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::{ - expressions::{ - parser::{ - move_formula::ref_is_in_area, - stringify::{to_string, to_string_displaced, DisplaceData}, - walk::forward_references, - }, - types::{Area, CellReferenceIndex, CellReferenceRC}, - }, - model::Model, -}; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(untagged, deny_unknown_fields)] -pub enum CellValue { - Value(String), - None, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct SetCellValue { - cell: CellReferenceIndex, - new_value: CellValue, - old_value: CellValue, -} - -impl Model { - #[allow(clippy::expect_used)] - pub(crate) fn shift_cell_formula( - &mut self, - sheet: u32, - row: i32, - column: i32, - displace_data: &DisplaceData, - ) { - if let Some(f) = self - .workbook - .worksheet(sheet) - .expect("Worksheet must exist") - .cell(row, column) - .expect("Cell must exist") - .get_formula() - { - let node = &self.parsed_formulas[sheet as usize][f as usize].clone(); - let cell_reference = CellReferenceRC { - sheet: self.workbook.worksheets[sheet as usize].get_name(), - row, - column, - }; - // FIXME: This is not a very performant way if the formula has changed :S. - let formula = to_string(node, &cell_reference); - let formula_displaced = to_string_displaced(node, &cell_reference, displace_data); - if formula != formula_displaced { - self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}")) - .expect("Failed to shift cell formula"); - } - } - } - - #[allow(clippy::expect_used)] - pub fn forward_references( - &mut self, - source_area: &Area, - target: &CellReferenceIndex, - ) -> Result, String> { - let mut diff_list: Vec = Vec::new(); - let target_area = &Area { - sheet: target.sheet, - row: target.row, - column: target.column, - width: source_area.width, - height: source_area.height, - }; - // Walk over every formula - let cells = self.get_all_cells(); - for cell in cells { - if let Some(f) = self - .workbook - .worksheet(cell.index) - .expect("Worksheet must exist") - .cell(cell.row, cell.column) - .expect("Cell must exist") - .get_formula() - { - let sheet = cell.index; - let row = cell.row; - let column = cell.column; - - // If cell is in the source or target area, skip - if ref_is_in_area(sheet, row, column, source_area) - || ref_is_in_area(sheet, row, column, target_area) - { - continue; - } - - // Get the formula - // Get a copy of the AST - let node = &mut self.parsed_formulas[sheet as usize][f as usize].clone(); - let cell_reference = CellReferenceRC { - sheet: self.workbook.worksheets[sheet as usize].get_name(), - column: cell.column, - row: cell.row, - }; - let context = CellReferenceIndex { sheet, column, row }; - let formula = to_string(node, &cell_reference); - let target_sheet_name = &self.workbook.worksheets[target.sheet as usize].name; - forward_references( - node, - &context, - source_area, - target.sheet, - target_sheet_name, - target.row, - target.column, - ); - - // If the string representation of the formula has changed update the cell - let updated_formula = to_string(node, &cell_reference); - if formula != updated_formula { - self.update_cell_with_formula( - sheet, - row, - column, - format!("={updated_formula}"), - )?; - // Update the diff list - diff_list.push(SetCellValue { - cell: CellReferenceIndex { sheet, column, row }, - new_value: CellValue::Value(format!("={}", updated_formula)), - old_value: CellValue::Value(format!("={}", formula)), - }); - } - } - } - Ok(diff_list) - } -} diff --git a/base/src/expressions/lexer/mod.rs b/base/src/expressions/lexer/mod.rs index 0e4ddedc..d629d0df 100644 --- a/base/src/expressions/lexer/mod.rs +++ b/base/src/expressions/lexer/mod.rs @@ -187,6 +187,7 @@ impl Lexer { ']' => TokenType::RightBracket, ':' => TokenType::Colon, ';' => TokenType::Semicolon, + '@' => TokenType::At, ',' => { if self.locale.numbers.symbols.decimal == "," { match self.consume_number(',') { diff --git a/base/src/expressions/lexer/test/mod.rs b/base/src/expressions/lexer/test/mod.rs index f795ba56..2da65f58 100644 --- a/base/src/expressions/lexer/test/mod.rs +++ b/base/src/expressions/lexer/test/mod.rs @@ -1,4 +1,5 @@ mod test_common; +mod test_implicit_intersection; mod test_language; mod test_locale; mod test_ranges; diff --git a/base/src/expressions/lexer/test/test_implicit_intersection.rs b/base/src/expressions/lexer/test/test_implicit_intersection.rs new file mode 100644 index 00000000..d0558507 --- /dev/null +++ b/base/src/expressions/lexer/test/test_implicit_intersection.rs @@ -0,0 +1,25 @@ +#![allow(clippy::unwrap_used)] + +use crate::expressions::{ + lexer::{Lexer, LexerMode}, + token::TokenType::*, +}; +use crate::language::get_language; +use crate::locale::get_locale; + +fn new_lexer(formula: &str) -> Lexer { + let locale = get_locale("en").unwrap(); + let language = get_language("en").unwrap(); + Lexer::new(formula, LexerMode::A1, locale, language) +} + +#[test] +fn sum_implicit_intersection() { + let mut lx = new_lexer("sum(@A1:A3)"); + assert_eq!(lx.next_token(), Ident("sum".to_string())); + assert_eq!(lx.next_token(), LeftParenthesis); + assert_eq!(lx.next_token(), At); + assert!(matches!(lx.next_token(), Range { .. })); + assert_eq!(lx.next_token(), RightParenthesis); + assert_eq!(lx.next_token(), EOF); +} diff --git a/base/src/expressions/parser/mod.rs b/base/src/expressions/parser/mod.rs index 62a4d47b..8cbec2bc 100644 --- a/base/src/expressions/parser/mod.rs +++ b/base/src/expressions/parser/mod.rs @@ -1,5 +1,5 @@ /*! -# GRAMAR +# GRAMMAR
 opComp   => '=' | '<' | '>' | '<=' } '>=' | '<>'
@@ -22,6 +22,7 @@ primary => '(' expr ')'
         => bool
         => bool()
         => error
+        => '@' expr
 
 f_args  => e (',' e)*
 
@@ -45,8 +46,8 @@ use super::utils::number_to_column; use token::OpCompare; pub mod move_formula; +pub mod static_analysis; pub mod stringify; -pub mod walk; #[cfg(test)] mod tests; @@ -81,6 +82,9 @@ fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option, String); + pub(crate) struct Reference<'a> { sheet_name: &'a Option, sheet_index: u32, @@ -164,9 +168,13 @@ pub enum Node { args: Vec, }, ArrayKind(Vec), - DefinedNameKind((String, Option)), + DefinedNameKind(DefinedNameS), TableNameKind(String), WrongVariableKind(String), + ImplicitIntersection { + automatic: bool, + child: Box, + }, CompareKind { kind: OpCompare, left: Box, @@ -189,7 +197,7 @@ pub enum Node { pub struct Parser { lexer: lexer::Lexer, worksheets: Vec, - defined_names: Vec<(String, Option)>, + defined_names: Vec, context: CellReferenceRC, tables: HashMap, } @@ -197,7 +205,7 @@ pub struct Parser { impl Parser { pub fn new( worksheets: Vec, - defined_names: Vec<(String, Option)>, + defined_names: Vec, tables: HashMap, ) -> Parser { let lexer = lexer::Lexer::new( @@ -228,7 +236,7 @@ impl Parser { pub fn set_worksheets_and_names( &mut self, worksheets: Vec, - defined_names: Vec<(String, Option)>, + defined_names: Vec, ) { self.worksheets = worksheets; self.defined_names = defined_names; @@ -252,17 +260,17 @@ impl Parser { // 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((Some(index), formula)): 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> { - for (df_name, df_scope) in &self.defined_names { + fn get_defined_name(&self, name: &str, sheet: u32) -> Option<(Option, String)> { + for (df_name, df_scope, df_formula) in &self.defined_names { if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) { - return Some(*df_scope); + return Some((*df_scope, df_formula.to_owned())); } } - for (df_name, df_scope) in &self.defined_names { + for (df_name, df_scope, df_formula) in &self.defined_names { if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() { - return Some(None); + return Some((None, df_formula.to_owned())); } } None @@ -604,6 +612,20 @@ impl Parser { args, }; } + if &name == "_xlfn.SINGLE" { + if args.len() != 1 { + return Node::ParseErrorKind { + formula: self.lexer.get_formula(), + position: self.lexer.get_position() as usize, + message: "Implicit Intersection requires just one argument" + .to_string(), + }; + } + return Node::ImplicitIntersection { + automatic: false, + child: Box::new(args[0].clone()), + }; + } return Node::InvalidFunctionKind { name, args }; } let context = &self.context; @@ -620,8 +642,8 @@ impl Parser { }; // 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)); + if let Some((scope, formula)) = self.get_defined_name(&name, context_sheet_index) { + return Node::DefinedNameKind((name, scope, formula)); } let name_lower = name.to_lowercase(); for table_name in self.tables.keys() { @@ -905,6 +927,16 @@ impl Parser { } } } + TokenType::At => { + let child = self.parse_expr(); + if let Node::ParseErrorKind { .. } = child { + return child; + } + Node::ImplicitIntersection { + automatic: false, + child: Box::new(child), + } + } } } diff --git a/base/src/expressions/parser/move_formula.rs b/base/src/expressions/parser/move_formula.rs index 5453cb0b..6b2bb1f6 100644 --- a/base/src/expressions/parser/move_formula.rs +++ b/base/src/expressions/parser/move_formula.rs @@ -375,7 +375,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String { } format!("{{{}}}", arguments) } - DefinedNameKind((name, _)) => name.to_string(), + DefinedNameKind((name, ..)) => name.to_string(), TableNameKind(name) => name.to_string(), WrongVariableKind(name) => name.to_string(), CompareKind { kind, left, right } => format!( @@ -395,5 +395,11 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String { position: _, } => formula.to_string(), EmptyArgKind => "".to_string(), + ImplicitIntersection { + automatic: _, + child, + } => { + format!("@{}", to_string_moved(child, move_context)) + } } } diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs new file mode 100644 index 00000000..280ac248 --- /dev/null +++ b/base/src/expressions/parser/static_analysis.rs @@ -0,0 +1,984 @@ +use crate::functions::Function; + +use super::Node; + +use once_cell::sync::Lazy; +use regex::Regex; + +#[allow(clippy::expect_used)] +static RE: Lazy = + Lazy::new(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid")); + +fn is_range_reference(s: &str) -> bool { + RE.is_match(s) +} + +/* + +# NOTES on the Implicit Intersection operator: @ + + Sometimes we obtain a range where we expected a single argument. This can happen: + + * As an argument of a function, eg: `SIN(A1:A5)` + * As the result of a computation of a formula `=A1:A5` + + In previous versions of the Friendly Giant the spreadsheet engine would perform an operation called _implicit intersection_ + that tries to find a single cell within the range. It works by picking a cell in the range that is the same row or the same column + as the cell. If there is just one we return that otherwise we return the `#REF!` error. + + Examples: + + * Siting on `C3` the formula `=D1:D5` will return `D3` + * Sitting on `C3` the formula `=D:D` will return `D3` + * Sitting on `C3` the formula `=A1:A7` will return `A3` + * Sitting on `C3` the formula `=A5:A8` will return `#REF!` + * Sitting on `C3` the formula `D1:G7` will return `#REF!` + + Today's version of the engine will result in a dynamic array spilling the result through several cells. + To force the old behaviour we can use the _implicit intersection operator_: @ + + * `=@A1:A7` or `=SIN(@A1:A7) + + When parsing formulas that come form old workbooks this is done automatically. + We call this version of the II operator the _automatic_ II operator. + + We can also insert the II operator in places where before was impossible: + + * `=SUM(@A1:A7)` + + This formulas will not be compatible with old versions of the engine. The FG will stringify this as `=SUM(_xlfn.SIMPLE(A1:A7))`. + */ + +/// Transverses the formula tree adding the implicit intersection operator in all arguments of functions that +/// expect a scalar but get a range. +/// * A:A => @A:A +/// * SIN(A1:D1) => SIN(@A1:D1) +/// +/// Assumes formula return a scalar +pub fn add_implicit_intersection(node: &mut Node, add: bool) { + match node { + Node::BooleanKind(_) + | Node::NumberKind(_) + | Node::StringKind(_) + | Node::ErrorKind(_) + | Node::EmptyArgKind + | Node::ParseErrorKind { .. } + | Node::WrongReferenceKind { .. } + | Node::WrongRangeKind { .. } + | Node::InvalidFunctionKind { .. } + | Node::ArrayKind(_) + | Node::ReferenceKind { .. } => {} + Node::ImplicitIntersection { child, .. } => { + // We need to check wether the II can be automatic or not + let mut new_node = child.as_ref().clone(); + add_implicit_intersection(&mut new_node, add); + if matches!(&new_node, Node::ImplicitIntersection { .. }) { + *node = new_node + } + } + Node::RangeKind { + row1, + column1, + row2, + column2, + sheet_name, + sheet_index, + absolute_row1, + absolute_column1, + absolute_row2, + absolute_column2, + } => { + if add { + *node = Node::ImplicitIntersection { + automatic: true, + child: Box::new(Node::RangeKind { + sheet_name: sheet_name.clone(), + sheet_index: *sheet_index, + absolute_row1: *absolute_row1, + absolute_column1: *absolute_column1, + row1: *row1, + column1: *column1, + absolute_row2: *absolute_row2, + absolute_column2: *absolute_column2, + row2: *row2, + column2: *column2, + }), + }; + } + } + Node::OpRangeKind { left, right } => { + if add { + *node = Node::ImplicitIntersection { + automatic: true, + child: Box::new(Node::OpRangeKind { + left: left.clone(), + right: right.clone(), + }), + } + } + } + + // operations + Node::UnaryKind { right, .. } => add_implicit_intersection(right, add), + Node::OpConcatenateKind { left, right } + | Node::OpSumKind { left, right, .. } + | Node::OpProductKind { left, right, .. } + | Node::OpPowerKind { left, right, .. } + | Node::CompareKind { left, right, .. } => { + add_implicit_intersection(left, add); + add_implicit_intersection(right, add); + } + + Node::DefinedNameKind(v) => { + if add { + // Not all defined names deserve the II operator + // For instance =Sheet1!A1 doesn't need to be intersected + if is_range_reference(&v.2) { + *node = Node::ImplicitIntersection { + automatic: true, + child: Box::new(Node::DefinedNameKind(v.to_owned())), + } + } + } + } + Node::WrongVariableKind(v) => { + if add { + *node = Node::ImplicitIntersection { + automatic: true, + child: Box::new(Node::WrongVariableKind(v.to_owned())), + } + } + } + Node::TableNameKind(_) => { + // noop for now + } + Node::FunctionKind { kind, args } => { + let arg_count = args.len(); + let signature = get_function_args_signature(kind, arg_count); + for index in 0..arg_count { + if matches!(signature[index], Signature::Scalar) + && matches!( + run_static_analysis_on_node(&args[index]), + StaticResult::Range(_, _) | StaticResult::Unknown + ) + { + add_implicit_intersection(&mut args[index], true); + } else { + add_implicit_intersection(&mut args[index], false); + } + } + if add + && matches!( + run_static_analysis_on_node(node), + StaticResult::Range(_, _) | StaticResult::Unknown + ) + { + *node = Node::ImplicitIntersection { + automatic: true, + child: Box::new(node.clone()), + } + } + } + }; +} + +pub(crate) enum StaticResult { + Scalar, + Array(i32, i32), + Range(i32, i32), + Unknown, + // TODO: What if one of the dimensions is known? + // what if the dimensions are unknown but bounded? +} + +fn static_analysis_op_nodes(left: &Node, right: &Node) -> StaticResult { + let lhs = run_static_analysis_on_node(left); + let rhs = run_static_analysis_on_node(right); + match (lhs, rhs) { + (StaticResult::Scalar, StaticResult::Scalar) => StaticResult::Scalar, + (StaticResult::Scalar, StaticResult::Array(a, b) | StaticResult::Range(a, b)) => { + StaticResult::Array(a, b) + } + + (StaticResult::Array(a, b) | StaticResult::Range(a, b), StaticResult::Scalar) => { + StaticResult::Array(a, b) + } + ( + StaticResult::Array(a1, b1) | StaticResult::Range(a1, b1), + StaticResult::Array(a2, b2) | StaticResult::Range(a2, b2), + ) => StaticResult::Array(a1.max(a2), b1.max(b2)), + + (_, StaticResult::Unknown) => StaticResult::Unknown, + (StaticResult::Unknown, _) => StaticResult::Unknown, + } +} + +// Returns: +// * Scalar if we can proof the result of the evaluation is a scalar +// * Array(a, b) if we know it will be an a x b array. +// * Range(a, b) if we know it will be a a x b range. +// * Unknown if we cannot guaranty either +fn run_static_analysis_on_node(node: &Node) -> StaticResult { + match node { + Node::BooleanKind(_) + | Node::NumberKind(_) + | Node::StringKind(_) + | Node::ErrorKind(_) + | Node::EmptyArgKind => StaticResult::Scalar, + Node::UnaryKind { right, .. } => run_static_analysis_on_node(right), + Node::ParseErrorKind { .. } => { + // StaticResult::Unknown is also valid + StaticResult::Scalar + } + Node::WrongReferenceKind { .. } => { + // StaticResult::Unknown is also valid + StaticResult::Scalar + } + Node::WrongRangeKind { .. } => { + // StaticResult::Unknown or Array is also valid + StaticResult::Scalar + } + Node::InvalidFunctionKind { .. } => { + // StaticResult::Unknown is also valid + StaticResult::Scalar + } + Node::ArrayKind(array) => { + let n = array.len() as i32; + // FIXME: This is a placeholder until we implement arrays + StaticResult::Array(n, 1) + } + Node::RangeKind { + row1, + column1, + row2, + column2, + .. + } => StaticResult::Range(row2 - row1, column2 - column1), + Node::OpRangeKind { .. } => { + // TODO: We could do a bit better here + StaticResult::Unknown + } + Node::ReferenceKind { .. } => StaticResult::Scalar, + + // binary operations + Node::OpConcatenateKind { left, right } => static_analysis_op_nodes(left, right), + Node::OpSumKind { left, right, .. } => static_analysis_op_nodes(left, right), + Node::OpProductKind { left, right, .. } => static_analysis_op_nodes(left, right), + Node::OpPowerKind { left, right, .. } => static_analysis_op_nodes(left, right), + Node::CompareKind { left, right, .. } => static_analysis_op_nodes(left, right), + + // defined names + Node::DefinedNameKind(_) => StaticResult::Unknown, + Node::WrongVariableKind(_) => StaticResult::Unknown, + Node::TableNameKind(_) => StaticResult::Unknown, + Node::FunctionKind { kind, args } => static_analysis_on_function(kind, args), + Node::ImplicitIntersection { .. } => StaticResult::Scalar, + } +} + +// If all the arguments are scalars the function will return a scalar +// If any of the arguments is a range or an array it will return an array +fn scalar_arguments(args: &[Node]) -> StaticResult { + let mut n = 0; + let mut m = 0; + for arg in args { + match run_static_analysis_on_node(arg) { + StaticResult::Scalar => { + // noop + } + StaticResult::Array(a, b) | StaticResult::Range(a, b) => { + n = n.max(a); + m = m.max(b); + } + StaticResult::Unknown => return StaticResult::Unknown, + } + } + if n == 0 && m == 0 { + return StaticResult::Scalar; + } + StaticResult::Array(n, m) +} + +// We only care if the function can return a range or not +fn not_implemented(_args: &[Node]) -> StaticResult { + StaticResult::Scalar +} + +fn static_analysis_offset(args: &[Node]) -> StaticResult { + // If first argument is a single cell reference and there are no4th and 5th argument, + // or they are 1, then it is a scalar + let arg_count = args.len(); + if arg_count < 3 { + // Actually an error + return StaticResult::Scalar; + } + if !matches!(args[0], Node::ReferenceKind { .. }) { + return StaticResult::Unknown; + } + if arg_count == 3 { + return StaticResult::Scalar; + } + match args[3] { + Node::NumberKind(f) => { + if f != 1.0 { + return StaticResult::Unknown; + } + } + _ => return StaticResult::Unknown, + }; + if arg_count == 4 { + return StaticResult::Scalar; + } + match args[4] { + Node::NumberKind(f) => { + if f != 1.0 { + return StaticResult::Unknown; + } + } + _ => return StaticResult::Unknown, + }; + StaticResult::Unknown +} + +// fn static_analysis_choose(_args: &[Node]) -> StaticResult { +// // We will always insert the @ in CHOOSE, but technically it is only needed if one of the elements is a range +// StaticResult::Unknown +// } + +fn static_analysis_indirect(_args: &[Node]) -> StaticResult { + // We will always insert the @, but we don't need to do that in every scenario` + StaticResult::Unknown +} + +fn static_analysis_index(_args: &[Node]) -> StaticResult { + // INDEX has two forms, but they are indistinguishable at parse time. + StaticResult::Unknown +} + +#[derive(Clone)] +enum Signature { + Scalar, + Vector, + Error, +} + +fn args_signature_no_args(arg_count: usize) -> Vec { + if arg_count == 0 { + vec![] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_scalars( + arg_count: usize, + required_count: usize, + optional_count: usize, +) -> Vec { + if arg_count >= required_count && arg_count <= required_count + optional_count { + vec![Signature::Scalar; arg_count] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_one_vector(arg_count: usize) -> Vec { + if arg_count == 1 { + vec![Signature::Vector] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_sumif(arg_count: usize) -> Vec { + if arg_count == 2 { + vec![Signature::Vector, Signature::Scalar] + } else if arg_count == 3 { + vec![Signature::Vector, Signature::Scalar, Signature::Vector] + } else { + vec![Signature::Error; arg_count] + } +} + +// 1 or none scalars +fn args_signature_sheet(arg_count: usize) -> Vec { + if arg_count == 0 { + vec![] + } else if arg_count == 1 { + vec![Signature::Scalar] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_hlookup(arg_count: usize) -> Vec { + if arg_count == 3 { + vec![Signature::Vector, Signature::Vector, Signature::Scalar] + } else if arg_count == 4 { + vec![ + Signature::Vector, + Signature::Vector, + Signature::Scalar, + Signature::Vector, + ] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_index(arg_count: usize) -> Vec { + if arg_count == 2 { + vec![Signature::Vector, Signature::Scalar] + } else if arg_count == 3 { + vec![Signature::Vector, Signature::Scalar, Signature::Scalar] + } else if arg_count == 4 { + vec![ + Signature::Vector, + Signature::Scalar, + Signature::Scalar, + Signature::Scalar, + ] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_lookup(arg_count: usize) -> Vec { + if arg_count == 2 { + vec![Signature::Vector, Signature::Vector] + } else if arg_count == 3 { + vec![Signature::Vector, Signature::Vector, Signature::Vector] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_match(arg_count: usize) -> Vec { + if arg_count == 2 { + vec![Signature::Vector, Signature::Vector] + } else if arg_count == 3 { + vec![Signature::Vector, Signature::Vector, Signature::Scalar] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_offset(arg_count: usize) -> Vec { + if arg_count == 3 { + vec![Signature::Vector, Signature::Scalar, Signature::Scalar] + } else if arg_count == 4 { + vec![ + Signature::Vector, + Signature::Scalar, + Signature::Scalar, + Signature::Scalar, + ] + } else if arg_count == 5 { + vec![ + Signature::Vector, + Signature::Scalar, + Signature::Scalar, + Signature::Scalar, + Signature::Scalar, + ] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_row(arg_count: usize) -> Vec { + if arg_count == 0 { + vec![] + } else if arg_count == 1 { + vec![Signature::Vector] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_xlookup(arg_count: usize) -> Vec { + if !(3..=6).contains(&arg_count) { + return vec![Signature::Error; arg_count]; + } + let mut result = vec![Signature::Scalar; arg_count]; + result[0] = Signature::Vector; + result[1] = Signature::Vector; + result[2] = Signature::Vector; + result +} + +fn args_signature_textafter(arg_count: usize) -> Vec { + if !(2..=6).contains(&arg_count) { + vec![Signature::Scalar; arg_count] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_textjoin(arg_count: usize) -> Vec { + if arg_count >= 3 { + let mut result = vec![Signature::Vector; arg_count]; + result[0] = Signature::Scalar; + result[1] = Signature::Scalar; + result + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_npv(arg_count: usize) -> Vec { + if arg_count < 2 { + return vec![Signature::Error; arg_count]; + } + let mut result = vec![Signature::Vector; arg_count]; + result[0] = Signature::Scalar; + result +} + +fn args_signature_irr(arg_count: usize) -> Vec { + if arg_count > 2 { + vec![Signature::Error; arg_count] + } else if arg_count == 1 { + vec![Signature::Vector] + } else { + vec![Signature::Vector, Signature::Scalar] + } +} + +fn args_signature_xirr(arg_count: usize) -> Vec { + if arg_count == 2 { + vec![Signature::Vector; arg_count] + } else if arg_count == 3 { + vec![Signature::Vector, Signature::Vector, Signature::Scalar] + } else { + vec![Signature::Error; arg_count] + } +} + +fn args_signature_mirr(arg_count: usize) -> Vec { + if arg_count != 3 { + vec![Signature::Error; arg_count] + } else { + vec![Signature::Vector, Signature::Scalar, Signature::Scalar] + } +} + +fn args_signature_xnpv(arg_count: usize) -> Vec { + if arg_count != 3 { + vec![Signature::Error; arg_count] + } else { + vec![Signature::Scalar, Signature::Vector, Signature::Vector] + } +} + +// FIXME: This is terrible duplications of efforts. We use the signature in at least three different places: +// 1. When computing the function +// 2. Checking the arguments to see if we need to insert the implicit intersection operator +// 3. Understanding the return value +// +// The signature of the functions should be defined only once + +// Given a function and a number of arguments this returns the arguments at each position +// are expected to be scalars or vectors (array/ranges). +// Sets signature::Error to all arguments if the number of arguments is incorrect. +fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec { + match kind { + Function::And => vec![Signature::Vector; arg_count], + Function::False => args_signature_no_args(arg_count), + Function::If => args_signature_scalars(arg_count, 2, 1), + Function::Iferror => args_signature_scalars(arg_count, 2, 0), + Function::Ifna => args_signature_scalars(arg_count, 2, 0), + Function::Ifs => vec![Signature::Scalar; arg_count], + Function::Not => args_signature_scalars(arg_count, 1, 0), + Function::Or => vec![Signature::Vector; arg_count], + Function::Switch => vec![Signature::Scalar; arg_count], + Function::True => args_signature_no_args(arg_count), + Function::Xor => vec![Signature::Vector; arg_count], + Function::Abs => args_signature_scalars(arg_count, 1, 0), + Function::Acos => args_signature_scalars(arg_count, 1, 0), + Function::Acosh => args_signature_scalars(arg_count, 1, 0), + Function::Asin => args_signature_scalars(arg_count, 1, 0), + Function::Asinh => args_signature_scalars(arg_count, 1, 0), + Function::Atan => args_signature_scalars(arg_count, 1, 0), + Function::Atan2 => args_signature_scalars(arg_count, 2, 0), + Function::Atanh => args_signature_scalars(arg_count, 1, 0), + Function::Choose => vec![Signature::Scalar; arg_count], + Function::Column => args_signature_row(arg_count), + Function::Columns => args_signature_one_vector(arg_count), + Function::Cos => args_signature_scalars(arg_count, 1, 0), + Function::Cosh => args_signature_scalars(arg_count, 1, 0), + Function::Max => vec![Signature::Vector; arg_count], + Function::Min => vec![Signature::Vector; arg_count], + Function::Pi => args_signature_no_args(arg_count), + Function::Power => args_signature_scalars(arg_count, 2, 0), + Function::Product => vec![Signature::Vector; arg_count], + Function::Round => args_signature_scalars(arg_count, 2, 0), + Function::Rounddown => args_signature_scalars(arg_count, 2, 0), + Function::Roundup => args_signature_scalars(arg_count, 2, 0), + Function::Sin => args_signature_scalars(arg_count, 1, 0), + Function::Sinh => args_signature_scalars(arg_count, 1, 0), + Function::Sqrt => args_signature_scalars(arg_count, 1, 0), + Function::Sqrtpi => args_signature_scalars(arg_count, 1, 0), + Function::Sum => vec![Signature::Vector; arg_count], + Function::Sumif => args_signature_sumif(arg_count), + Function::Sumifs => vec![Signature::Vector; arg_count], + Function::Tan => args_signature_scalars(arg_count, 1, 0), + Function::Tanh => args_signature_scalars(arg_count, 1, 0), + Function::ErrorType => args_signature_scalars(arg_count, 1, 0), + Function::Isblank => args_signature_scalars(arg_count, 1, 0), + Function::Iserr => args_signature_scalars(arg_count, 1, 0), + Function::Iserror => args_signature_scalars(arg_count, 1, 0), + Function::Iseven => args_signature_scalars(arg_count, 1, 0), + Function::Isformula => args_signature_scalars(arg_count, 1, 0), + Function::Islogical => args_signature_scalars(arg_count, 1, 0), + Function::Isna => args_signature_scalars(arg_count, 1, 0), + Function::Isnontext => args_signature_scalars(arg_count, 1, 0), + Function::Isnumber => args_signature_scalars(arg_count, 1, 0), + Function::Isodd => args_signature_scalars(arg_count, 1, 0), + Function::Isref => args_signature_one_vector(arg_count), + Function::Istext => args_signature_scalars(arg_count, 1, 0), + Function::Na => args_signature_no_args(arg_count), + Function::Sheet => args_signature_sheet(arg_count), + Function::Type => args_signature_one_vector(arg_count), + Function::Hlookup => args_signature_hlookup(arg_count), + Function::Index => args_signature_index(arg_count), + Function::Indirect => args_signature_scalars(arg_count, 1, 0), + Function::Lookup => args_signature_lookup(arg_count), + Function::Match => args_signature_match(arg_count), + Function::Offset => args_signature_offset(arg_count), + Function::Row => args_signature_row(arg_count), + Function::Rows => args_signature_one_vector(arg_count), + Function::Vlookup => args_signature_hlookup(arg_count), + Function::Xlookup => args_signature_xlookup(arg_count), + Function::Concat => vec![Signature::Vector; arg_count], + Function::Concatenate => vec![Signature::Scalar; arg_count], + Function::Exact => args_signature_scalars(arg_count, 2, 0), + Function::Find => args_signature_scalars(arg_count, 2, 1), + Function::Left => args_signature_scalars(arg_count, 1, 1), + Function::Len => args_signature_scalars(arg_count, 1, 0), + Function::Lower => args_signature_scalars(arg_count, 1, 0), + Function::Mid => args_signature_scalars(arg_count, 3, 0), + Function::Rept => args_signature_scalars(arg_count, 2, 0), + Function::Right => args_signature_scalars(arg_count, 2, 1), + Function::Search => args_signature_scalars(arg_count, 2, 1), + Function::Substitute => args_signature_scalars(arg_count, 3, 1), + Function::T => args_signature_scalars(arg_count, 1, 0), + Function::Text => args_signature_scalars(arg_count, 2, 0), + Function::Textafter => args_signature_textafter(arg_count), + Function::Textbefore => args_signature_textafter(arg_count), + Function::Textjoin => args_signature_textjoin(arg_count), + Function::Trim => args_signature_scalars(arg_count, 1, 0), + Function::Upper => args_signature_scalars(arg_count, 1, 0), + Function::Value => args_signature_scalars(arg_count, 1, 0), + Function::Valuetotext => args_signature_scalars(arg_count, 1, 1), + Function::Average => vec![Signature::Vector; arg_count], + Function::Averagea => vec![Signature::Vector; arg_count], + Function::Averageif => args_signature_sumif(arg_count), + Function::Averageifs => vec![Signature::Vector; arg_count], + Function::Count => vec![Signature::Vector; arg_count], + Function::Counta => vec![Signature::Vector; arg_count], + Function::Countblank => vec![Signature::Vector; arg_count], + Function::Countif => args_signature_sumif(arg_count), + Function::Countifs => vec![Signature::Vector; arg_count], + Function::Maxifs => vec![Signature::Vector; arg_count], + Function::Minifs => vec![Signature::Vector; arg_count], + Function::Date => args_signature_scalars(arg_count, 3, 0), + Function::Day => args_signature_scalars(arg_count, 1, 0), + Function::Edate => args_signature_scalars(arg_count, 2, 0), + Function::Eomonth => args_signature_scalars(arg_count, 2, 0), + Function::Month => args_signature_scalars(arg_count, 1, 0), + Function::Now => args_signature_no_args(arg_count), + Function::Today => args_signature_no_args(arg_count), + Function::Year => args_signature_scalars(arg_count, 1, 0), + Function::Cumipmt => args_signature_scalars(arg_count, 6, 0), + Function::Cumprinc => args_signature_scalars(arg_count, 6, 0), + Function::Db => args_signature_scalars(arg_count, 4, 1), + Function::Ddb => args_signature_scalars(arg_count, 4, 1), + Function::Dollarde => args_signature_scalars(arg_count, 2, 0), + Function::Dollarfr => args_signature_scalars(arg_count, 2, 0), + Function::Effect => args_signature_scalars(arg_count, 2, 0), + Function::Fv => args_signature_scalars(arg_count, 3, 2), + Function::Ipmt => args_signature_scalars(arg_count, 4, 2), + Function::Irr => args_signature_irr(arg_count), + Function::Ispmt => args_signature_scalars(arg_count, 4, 0), + Function::Mirr => args_signature_mirr(arg_count), + Function::Nominal => args_signature_scalars(arg_count, 2, 0), + Function::Nper => args_signature_scalars(arg_count, 3, 2), + Function::Npv => args_signature_npv(arg_count), + Function::Pduration => args_signature_scalars(arg_count, 3, 0), + Function::Pmt => args_signature_scalars(arg_count, 3, 2), + Function::Ppmt => args_signature_scalars(arg_count, 4, 2), + Function::Pv => args_signature_scalars(arg_count, 3, 2), + Function::Rate => args_signature_scalars(arg_count, 3, 3), + Function::Rri => args_signature_scalars(arg_count, 3, 0), + Function::Sln => args_signature_scalars(arg_count, 3, 0), + Function::Syd => args_signature_scalars(arg_count, 4, 0), + Function::Tbilleq => args_signature_scalars(arg_count, 3, 0), + Function::Tbillprice => args_signature_scalars(arg_count, 3, 0), + Function::Tbillyield => args_signature_scalars(arg_count, 3, 0), + Function::Xirr => args_signature_xirr(arg_count), + Function::Xnpv => args_signature_xnpv(arg_count), + Function::Besseli => args_signature_scalars(arg_count, 2, 0), + Function::Besselj => args_signature_scalars(arg_count, 2, 0), + Function::Besselk => args_signature_scalars(arg_count, 2, 0), + Function::Bessely => args_signature_scalars(arg_count, 2, 0), + Function::Erf => args_signature_scalars(arg_count, 1, 1), + Function::Erfc => args_signature_scalars(arg_count, 1, 0), + Function::ErfcPrecise => args_signature_scalars(arg_count, 1, 0), + Function::ErfPrecise => args_signature_scalars(arg_count, 1, 0), + Function::Bin2dec => args_signature_scalars(arg_count, 1, 0), + Function::Bin2hex => args_signature_scalars(arg_count, 1, 0), + Function::Bin2oct => args_signature_scalars(arg_count, 1, 0), + Function::Dec2Bin => args_signature_scalars(arg_count, 1, 0), + Function::Dec2hex => args_signature_scalars(arg_count, 1, 0), + Function::Dec2oct => args_signature_scalars(arg_count, 1, 0), + Function::Hex2bin => args_signature_scalars(arg_count, 1, 0), + Function::Hex2dec => args_signature_scalars(arg_count, 1, 0), + Function::Hex2oct => args_signature_scalars(arg_count, 1, 0), + Function::Oct2bin => args_signature_scalars(arg_count, 1, 0), + Function::Oct2dec => args_signature_scalars(arg_count, 1, 0), + Function::Oct2hex => args_signature_scalars(arg_count, 1, 0), + Function::Bitand => args_signature_scalars(arg_count, 2, 0), + Function::Bitlshift => args_signature_scalars(arg_count, 2, 0), + Function::Bitor => args_signature_scalars(arg_count, 2, 0), + Function::Bitrshift => args_signature_scalars(arg_count, 2, 0), + Function::Bitxor => args_signature_scalars(arg_count, 2, 0), + Function::Complex => args_signature_scalars(arg_count, 2, 1), + Function::Imabs => args_signature_scalars(arg_count, 1, 0), + Function::Imaginary => args_signature_scalars(arg_count, 1, 0), + Function::Imargument => args_signature_scalars(arg_count, 1, 0), + Function::Imconjugate => args_signature_scalars(arg_count, 1, 0), + Function::Imcos => args_signature_scalars(arg_count, 1, 0), + Function::Imcosh => args_signature_scalars(arg_count, 1, 0), + Function::Imcot => args_signature_scalars(arg_count, 1, 0), + Function::Imcsc => args_signature_scalars(arg_count, 1, 0), + Function::Imcsch => args_signature_scalars(arg_count, 1, 0), + Function::Imdiv => args_signature_scalars(arg_count, 2, 0), + Function::Imexp => args_signature_scalars(arg_count, 1, 0), + Function::Imln => args_signature_scalars(arg_count, 1, 0), + Function::Imlog10 => args_signature_scalars(arg_count, 1, 0), + Function::Imlog2 => args_signature_scalars(arg_count, 1, 0), + Function::Impower => args_signature_scalars(arg_count, 2, 0), + Function::Improduct => args_signature_scalars(arg_count, 2, 0), + Function::Imreal => args_signature_scalars(arg_count, 1, 0), + Function::Imsec => args_signature_scalars(arg_count, 1, 0), + Function::Imsech => args_signature_scalars(arg_count, 1, 0), + Function::Imsin => args_signature_scalars(arg_count, 1, 0), + Function::Imsinh => args_signature_scalars(arg_count, 1, 0), + Function::Imsqrt => args_signature_scalars(arg_count, 1, 0), + Function::Imsub => args_signature_scalars(arg_count, 2, 0), + Function::Imsum => args_signature_scalars(arg_count, 2, 0), + Function::Imtan => args_signature_scalars(arg_count, 1, 0), + Function::Convert => args_signature_scalars(arg_count, 3, 0), + Function::Delta => args_signature_scalars(arg_count, 1, 1), + Function::Gestep => args_signature_scalars(arg_count, 1, 1), + Function::Subtotal => args_signature_npv(arg_count), + Function::Rand => args_signature_no_args(arg_count), + Function::Randbetween => args_signature_scalars(arg_count, 2, 0), + Function::Formulatext => args_signature_scalars(arg_count, 1, 0), + Function::Unicode => args_signature_scalars(arg_count, 1, 0), + Function::Geomean => vec![Signature::Vector; arg_count], + } +} + +// Returns the type of the result (Scalar, Array or Range) depending on the arguments +fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { + match kind { + Function::And => StaticResult::Scalar, + Function::False => StaticResult::Scalar, + Function::If => scalar_arguments(args), + Function::Iferror => scalar_arguments(args), + Function::Ifna => scalar_arguments(args), + Function::Ifs => not_implemented(args), + Function::Not => StaticResult::Scalar, + Function::Or => StaticResult::Scalar, + Function::Switch => not_implemented(args), + Function::True => StaticResult::Scalar, + Function::Xor => StaticResult::Scalar, + Function::Abs => scalar_arguments(args), + Function::Acos => scalar_arguments(args), + Function::Acosh => scalar_arguments(args), + Function::Asin => scalar_arguments(args), + Function::Asinh => scalar_arguments(args), + Function::Atan => scalar_arguments(args), + Function::Atan2 => scalar_arguments(args), + Function::Atanh => scalar_arguments(args), + Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell), + Function::Column => not_implemented(args), + Function::Columns => not_implemented(args), + Function::Cos => scalar_arguments(args), + Function::Cosh => scalar_arguments(args), + Function::Max => StaticResult::Scalar, + Function::Min => StaticResult::Scalar, + Function::Pi => StaticResult::Scalar, + Function::Power => scalar_arguments(args), + Function::Product => not_implemented(args), + Function::Round => scalar_arguments(args), + Function::Rounddown => scalar_arguments(args), + Function::Roundup => scalar_arguments(args), + Function::Sin => scalar_arguments(args), + Function::Sinh => scalar_arguments(args), + Function::Sqrt => scalar_arguments(args), + Function::Sqrtpi => StaticResult::Scalar, + Function::Sum => StaticResult::Scalar, + Function::Sumif => not_implemented(args), + Function::Sumifs => not_implemented(args), + Function::Tan => scalar_arguments(args), + Function::Tanh => scalar_arguments(args), + Function::ErrorType => not_implemented(args), + Function::Isblank => not_implemented(args), + Function::Iserr => not_implemented(args), + Function::Iserror => not_implemented(args), + Function::Iseven => not_implemented(args), + Function::Isformula => not_implemented(args), + Function::Islogical => not_implemented(args), + Function::Isna => not_implemented(args), + Function::Isnontext => not_implemented(args), + Function::Isnumber => not_implemented(args), + Function::Isodd => not_implemented(args), + Function::Isref => not_implemented(args), + Function::Istext => not_implemented(args), + Function::Na => StaticResult::Scalar, + Function::Sheet => StaticResult::Scalar, + Function::Type => not_implemented(args), + Function::Hlookup => not_implemented(args), + Function::Index => static_analysis_index(args), + Function::Indirect => static_analysis_indirect(args), + Function::Lookup => not_implemented(args), + Function::Match => not_implemented(args), + Function::Offset => static_analysis_offset(args), + // FIXME: Row could return an array + Function::Row => StaticResult::Scalar, + Function::Rows => not_implemented(args), + Function::Vlookup => not_implemented(args), + Function::Xlookup => not_implemented(args), + Function::Concat => not_implemented(args), + Function::Concatenate => not_implemented(args), + Function::Exact => not_implemented(args), + Function::Find => not_implemented(args), + Function::Left => not_implemented(args), + Function::Len => not_implemented(args), + Function::Lower => not_implemented(args), + Function::Mid => not_implemented(args), + Function::Rept => not_implemented(args), + Function::Right => not_implemented(args), + Function::Search => not_implemented(args), + Function::Substitute => not_implemented(args), + Function::T => not_implemented(args), + Function::Text => not_implemented(args), + Function::Textafter => not_implemented(args), + Function::Textbefore => not_implemented(args), + Function::Textjoin => not_implemented(args), + Function::Trim => not_implemented(args), + Function::Unicode => not_implemented(args), + Function::Upper => not_implemented(args), + Function::Value => not_implemented(args), + Function::Valuetotext => not_implemented(args), + Function::Average => not_implemented(args), + Function::Averagea => not_implemented(args), + Function::Averageif => not_implemented(args), + Function::Averageifs => not_implemented(args), + Function::Count => not_implemented(args), + Function::Counta => not_implemented(args), + Function::Countblank => not_implemented(args), + Function::Countif => not_implemented(args), + Function::Countifs => not_implemented(args), + Function::Maxifs => not_implemented(args), + Function::Minifs => not_implemented(args), + Function::Date => not_implemented(args), + Function::Day => not_implemented(args), + Function::Edate => not_implemented(args), + Function::Month => not_implemented(args), + Function::Now => not_implemented(args), + Function::Today => not_implemented(args), + Function::Year => not_implemented(args), + Function::Cumipmt => not_implemented(args), + Function::Cumprinc => not_implemented(args), + Function::Db => not_implemented(args), + Function::Ddb => not_implemented(args), + Function::Dollarde => not_implemented(args), + Function::Dollarfr => not_implemented(args), + Function::Effect => not_implemented(args), + Function::Fv => not_implemented(args), + Function::Ipmt => not_implemented(args), + Function::Irr => not_implemented(args), + Function::Ispmt => not_implemented(args), + Function::Mirr => not_implemented(args), + Function::Nominal => not_implemented(args), + Function::Nper => not_implemented(args), + Function::Npv => not_implemented(args), + Function::Pduration => not_implemented(args), + Function::Pmt => not_implemented(args), + Function::Ppmt => not_implemented(args), + Function::Pv => not_implemented(args), + Function::Rate => not_implemented(args), + Function::Rri => not_implemented(args), + Function::Sln => not_implemented(args), + Function::Syd => not_implemented(args), + Function::Tbilleq => not_implemented(args), + Function::Tbillprice => not_implemented(args), + Function::Tbillyield => not_implemented(args), + Function::Xirr => not_implemented(args), + Function::Xnpv => not_implemented(args), + Function::Besseli => scalar_arguments(args), + Function::Besselj => scalar_arguments(args), + Function::Besselk => scalar_arguments(args), + Function::Bessely => scalar_arguments(args), + Function::Erf => scalar_arguments(args), + Function::Erfc => scalar_arguments(args), + Function::ErfcPrecise => scalar_arguments(args), + Function::ErfPrecise => scalar_arguments(args), + Function::Bin2dec => scalar_arguments(args), + Function::Bin2hex => scalar_arguments(args), + Function::Bin2oct => scalar_arguments(args), + Function::Dec2Bin => scalar_arguments(args), + Function::Dec2hex => scalar_arguments(args), + Function::Dec2oct => scalar_arguments(args), + Function::Hex2bin => scalar_arguments(args), + Function::Hex2dec => scalar_arguments(args), + Function::Hex2oct => scalar_arguments(args), + Function::Oct2bin => scalar_arguments(args), + Function::Oct2dec => scalar_arguments(args), + Function::Oct2hex => scalar_arguments(args), + Function::Bitand => scalar_arguments(args), + Function::Bitlshift => scalar_arguments(args), + Function::Bitor => scalar_arguments(args), + Function::Bitrshift => scalar_arguments(args), + Function::Bitxor => scalar_arguments(args), + Function::Complex => scalar_arguments(args), + Function::Imabs => scalar_arguments(args), + Function::Imaginary => scalar_arguments(args), + Function::Imargument => scalar_arguments(args), + Function::Imconjugate => scalar_arguments(args), + Function::Imcos => scalar_arguments(args), + Function::Imcosh => scalar_arguments(args), + Function::Imcot => scalar_arguments(args), + Function::Imcsc => scalar_arguments(args), + Function::Imcsch => scalar_arguments(args), + Function::Imdiv => scalar_arguments(args), + Function::Imexp => scalar_arguments(args), + Function::Imln => scalar_arguments(args), + Function::Imlog10 => scalar_arguments(args), + Function::Imlog2 => scalar_arguments(args), + Function::Impower => scalar_arguments(args), + Function::Improduct => scalar_arguments(args), + Function::Imreal => scalar_arguments(args), + Function::Imsec => scalar_arguments(args), + Function::Imsech => scalar_arguments(args), + Function::Imsin => scalar_arguments(args), + Function::Imsinh => scalar_arguments(args), + Function::Imsqrt => scalar_arguments(args), + Function::Imsub => scalar_arguments(args), + Function::Imsum => scalar_arguments(args), + Function::Imtan => scalar_arguments(args), + Function::Convert => not_implemented(args), + Function::Delta => not_implemented(args), + Function::Gestep => not_implemented(args), + Function::Subtotal => not_implemented(args), + Function::Rand => not_implemented(args), + Function::Randbetween => scalar_arguments(args), + Function::Eomonth => scalar_arguments(args), + Function::Formulatext => not_implemented(args), + Function::Geomean => not_implemented(args), + } +} diff --git a/base/src/expressions/parser/stringify.rs b/base/src/expressions/parser/stringify.rs index 00f2d5f4..0b878655 100644 --- a/base/src/expressions/parser/stringify.rs +++ b/base/src/expressions/parser/stringify.rs @@ -1,5 +1,6 @@ use super::{super::utils::quote_name, Node, Reference}; use crate::constants::{LAST_COLUMN, LAST_ROW}; +use crate::expressions::parser::static_analysis::add_implicit_intersection; use crate::expressions::token::OpUnary; use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str}; @@ -34,30 +35,33 @@ pub enum DisplaceData { None, } +/// This is the internal mode in IronCalc pub fn to_rc_format(node: &Node) -> String { stringify(node, None, &DisplaceData::None, false) } -pub fn to_string_displaced( - node: &Node, - context: &CellReferenceRC, - displace_data: &DisplaceData, -) -> String { - stringify(node, Some(context), displace_data, false) -} - +/// This is the mode used to display the formula in the UI pub fn to_string(node: &Node, context: &CellReferenceRC) -> String { stringify(node, Some(context), &DisplaceData::None, false) } +/// This is the mode used to export the formula to Excel pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String { stringify(node, Some(context), &DisplaceData::None, true) } +pub fn to_string_displaced( + node: &Node, + context: &CellReferenceRC, + displace_data: &DisplaceData, +) -> String { + stringify(node, Some(context), displace_data, false) +} + /// Converts a local reference to a string applying some displacement if needed. /// It uses A1 style if context is not None. If context is None it uses R1C1 style /// If full_row is true then the row details will be omitted in the A1 case -/// If full_colum is true then column details will be omitted. +/// If full_column is true then column details will be omitted. pub(crate) fn stringify_reference( context: Option<&CellReferenceRC>, displace_data: &DisplaceData, @@ -235,7 +239,7 @@ fn format_function( args: &Vec, context: Option<&CellReferenceRC>, displace_data: &DisplaceData, - use_original_name: bool, + export_to_excel: bool, ) -> String { let mut first = true; let mut arguments = "".to_string(); @@ -244,11 +248,11 @@ fn format_function( arguments = format!( "{},{}", arguments, - stringify(el, context, displace_data, use_original_name) + stringify(el, context, displace_data, export_to_excel) ); } else { first = false; - arguments = stringify(el, context, displace_data, use_original_name); + arguments = stringify(el, context, displace_data, export_to_excel); } } format!("{}({})", name, arguments) @@ -258,7 +262,7 @@ fn stringify( node: &Node, context: Option<&CellReferenceRC>, displace_data: &DisplaceData, - use_original_name: bool, + export_to_excel: bool, ) -> String { use self::Node::*; match node { @@ -407,52 +411,52 @@ fn stringify( } OpRangeKind { left, right } => format!( "{}:{}", - stringify(left, context, displace_data, use_original_name), - stringify(right, context, displace_data, use_original_name) + stringify(left, context, displace_data, export_to_excel), + stringify(right, context, displace_data, export_to_excel) ), OpConcatenateKind { left, right } => format!( "{}&{}", - stringify(left, context, displace_data, use_original_name), - stringify(right, context, displace_data, use_original_name) + stringify(left, context, displace_data, export_to_excel), + stringify(right, context, displace_data, export_to_excel) ), CompareKind { kind, left, right } => format!( "{}{}{}", - stringify(left, context, displace_data, use_original_name), + stringify(left, context, displace_data, export_to_excel), kind, - stringify(right, context, displace_data, use_original_name) + stringify(right, context, displace_data, export_to_excel) ), OpSumKind { kind, left, right } => format!( "{}{}{}", - stringify(left, context, displace_data, use_original_name), + stringify(left, context, displace_data, export_to_excel), kind, - stringify(right, context, displace_data, use_original_name) + stringify(right, context, displace_data, export_to_excel) ), OpProductKind { kind, left, right } => { let x = match **left { OpSumKind { .. } => format!( "({})", - stringify(left, context, displace_data, use_original_name) + stringify(left, context, displace_data, export_to_excel) ), CompareKind { .. } => format!( "({})", - stringify(left, context, displace_data, use_original_name) + stringify(left, context, displace_data, export_to_excel) ), - _ => stringify(left, context, displace_data, use_original_name), + _ => stringify(left, context, displace_data, export_to_excel), }; let y = match **right { OpSumKind { .. } => format!( "({})", - stringify(right, context, displace_data, use_original_name) + stringify(right, context, displace_data, export_to_excel) ), CompareKind { .. } => format!( "({})", - stringify(right, context, displace_data, use_original_name) + stringify(right, context, displace_data, export_to_excel) ), OpProductKind { .. } => format!( "({})", - stringify(right, context, displace_data, use_original_name) + stringify(right, context, displace_data, export_to_excel) ), - _ => stringify(right, context, displace_data, use_original_name), + _ => stringify(right, context, displace_data, export_to_excel), }; format!("{}{}{}", x, kind, y) } @@ -467,9 +471,7 @@ fn stringify( | DefinedNameKind(_) | TableNameKind(_) | WrongVariableKind(_) - | WrongRangeKind { .. } => { - stringify(left, context, displace_data, use_original_name) - } + | WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel), OpRangeKind { .. } | OpConcatenateKind { .. } | OpProductKind { .. } @@ -482,9 +484,10 @@ fn stringify( | ParseErrorKind { .. } | OpSumKind { .. } | CompareKind { .. } + | ImplicitIntersection { .. } | EmptyArgKind => format!( "({})", - stringify(left, context, displace_data, use_original_name) + stringify(left, context, displace_data, export_to_excel) ), }; let y = match **right { @@ -498,7 +501,7 @@ fn stringify( | TableNameKind(_) | WrongVariableKind(_) | WrongRangeKind { .. } => { - stringify(right, context, displace_data, use_original_name) + stringify(right, context, displace_data, export_to_excel) } OpRangeKind { .. } | OpConcatenateKind { .. } @@ -512,23 +515,24 @@ fn stringify( | ParseErrorKind { .. } | OpSumKind { .. } | CompareKind { .. } + | ImplicitIntersection { .. } | EmptyArgKind => format!( "({})", - stringify(right, context, displace_data, use_original_name) + stringify(right, context, displace_data, export_to_excel) ), }; format!("{}^{}", x, y) } InvalidFunctionKind { name, args } => { - format_function(name, args, context, displace_data, use_original_name) + format_function(name, args, context, displace_data, export_to_excel) } FunctionKind { kind, args } => { - let name = if use_original_name { + let name = if export_to_excel { kind.to_xlsx_string() } else { kind.to_string() }; - format_function(&name, args, context, displace_data, use_original_name) + format_function(&name, args, context, displace_data, export_to_excel) } ArrayKind(args) => { let mut first = true; @@ -538,29 +542,29 @@ fn stringify( arguments = format!( "{},{}", arguments, - stringify(el, context, displace_data, use_original_name) + stringify(el, context, displace_data, export_to_excel) ); } else { first = false; - arguments = stringify(el, context, displace_data, use_original_name); + arguments = stringify(el, context, displace_data, export_to_excel); } } format!("{{{}}}", arguments) } TableNameKind(value) => value.to_string(), - DefinedNameKind((name, _)) => name.to_string(), + DefinedNameKind((name, ..)) => name.to_string(), WrongVariableKind(name) => name.to_string(), UnaryKind { kind, right } => match kind { OpUnary::Minus => { format!( "-{}", - stringify(right, context, displace_data, use_original_name) + stringify(right, context, displace_data, export_to_excel) ) } OpUnary::Percentage => { format!( "{}%", - stringify(right, context, displace_data, use_original_name) + stringify(right, context, displace_data, export_to_excel) ) } }, @@ -571,6 +575,29 @@ fn stringify( message: _, } => formula.to_string(), EmptyArgKind => "".to_string(), + ImplicitIntersection { + automatic: _, + child, + } => { + if export_to_excel { + // We need to check wether the II can be automatic or not + let mut new_node = child.as_ref().clone(); + + add_implicit_intersection(&mut new_node, true); + if matches!(&new_node, Node::ImplicitIntersection { .. }) { + return stringify(child, context, displace_data, export_to_excel); + } + + return format!( + "_xlfn.SINGLE({})", + stringify(child, context, displace_data, export_to_excel) + ); + } + format!( + "@{}", + stringify(child, context, displace_data, export_to_excel) + ) + } } } @@ -658,6 +685,12 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name: Node::UnaryKind { kind: _, right } => { rename_sheet_in_node(right, sheet_index, new_name); } + Node::ImplicitIntersection { + automatic: _, + child, + } => { + rename_sheet_in_node(child, sheet_index, new_name); + } // Do nothing Node::BooleanKind(_) => {} @@ -681,7 +714,7 @@ pub(crate) fn rename_defined_name_in_node( ) { match node { // Rename - Node::DefinedNameKind((n, s)) => { + Node::DefinedNameKind((n, s, _)) => { if name.to_lowercase() == n.to_lowercase() && *s == scope { *n = new_name.to_string(); } @@ -736,6 +769,12 @@ pub(crate) fn rename_defined_name_in_node( Node::UnaryKind { kind: _, right } => { rename_defined_name_in_node(right, name, scope, new_name); } + Node::ImplicitIntersection { + automatic: _, + child, + } => { + rename_defined_name_in_node(child, name, scope, new_name); + } // Do nothing Node::BooleanKind(_) => {} diff --git a/base/src/expressions/parser/tests/mod.rs b/base/src/expressions/parser/tests/mod.rs index 76613380..04d578ae 100644 --- a/base/src/expressions/parser/tests/mod.rs +++ b/base/src/expressions/parser/tests/mod.rs @@ -1,3 +1,4 @@ +mod test_add_implicit_intersection; mod test_general; mod test_issue_155; mod test_move_formula; diff --git a/base/src/expressions/parser/tests/test_add_implicit_intersection.rs b/base/src/expressions/parser/tests/test_add_implicit_intersection.rs new file mode 100644 index 00000000..53abbe56 --- /dev/null +++ b/base/src/expressions/parser/tests/test_add_implicit_intersection.rs @@ -0,0 +1,80 @@ +use std::collections::HashMap; + +use crate::expressions::{ + parser::{ + stringify::{to_excel_string, to_string}, + Parser, + }, + types::CellReferenceRC, +}; + +use crate::expressions::parser::static_analysis::add_implicit_intersection; + +#[test] +fn simple_test() { + let worksheets = vec!["Sheet1".to_string()]; + let mut parser = Parser::new(worksheets, vec![], HashMap::new()); + + // Reference cell is Sheet1!A1 + let cell_reference = CellReferenceRC { + sheet: "Sheet1".to_string(), + row: 1, + column: 1, + }; + let cases = vec![ + ("A1:A10*SUM(A1:A10)", "@A1:A10*SUM(A1:A10)"), + ("A1:A10", "@A1:A10"), + // Math and trigonometry functions + ("SUM(A1:A10)", "SUM(A1:A10)"), + ("SIN(A1:A10)", "SIN(@A1:A10)"), + ("COS(A1:A10)", "COS(@A1:A10)"), + ("TAN(A1:A10)", "TAN(@A1:A10)"), + ("ASIN(A1:A10)", "ASIN(@A1:A10)"), + ("ACOS(A1:A10)", "ACOS(@A1:A10)"), + ("ATAN(A1:A10)", "ATAN(@A1:A10)"), + ("SINH(A1:A10)", "SINH(@A1:A10)"), + ("COSH(A1:A10)", "COSH(@A1:A10)"), + ("TANH(A1:A10)", "TANH(@A1:A10)"), + ("ASINH(A1:A10)", "ASINH(@A1:A10)"), + ("ACOSH(A1:A10)", "ACOSH(@A1:A10)"), + ("ATANH(A1:A10)", "ATANH(@A1:A10)"), + ("ATAN2(A1:A10,B1:B10)", "ATAN2(@A1:A10,@B1:B10)"), + ("ATAN2(A1:A10,A1)", "ATAN2(@A1:A10,A1)"), + ("SQRT(A1:A10)", "SQRT(@A1:A10)"), + ("SQRTPI(A1:A10)", "SQRTPI(@A1:A10)"), + ("POWER(A1:A10,A1)", "POWER(@A1:A10,A1)"), + ("POWER(A1:A10,B1:B10)", "POWER(@A1:A10,@B1:B10)"), + ("MAX(A1:A10)", "MAX(A1:A10)"), + ("MIN(A1:A10)", "MIN(A1:A10)"), + ("ABS(A1:A10)", "ABS(@A1:A10)"), + ("FALSE()", "FALSE()"), + ("TRUE()", "TRUE()"), + // Defined names + ("BADNMAE", "@BADNMAE"), + // Logical + ("AND(A1:A10)", "AND(A1:A10)"), + ("OR(A1:A10)", "OR(A1:A10)"), + ("NOT(A1:A10)", "NOT(@A1:A10)"), + ("IF(A1:A10,B1:B10,C1:C10)", "IF(@A1:A10,@B1:B10,@C1:C10)"), + // Information + // ("ISBLANK(A1:A10)", "ISBLANK(A1:A10)"), + // ("ISERR(A1:A10)", "ISERR(A1:A10)"), + // ("ISERROR(A1:A10)", "ISERROR(A1:A10)"), + // ("ISEVEN(A1:A10)", "ISEVEN(A1:A10)"), + // ("ISLOGICAL(A1:A10)", "ISLOGICAL(A1:A10)"), + // ("ISNA(A1:A10)", "ISNA(A1:A10)"), + // ("ISNONTEXT(A1:A10)", "ISNONTEXT(A1:A10)"), + // ("ISNUMBER(A1:A10)", "ISNUMBER(A1:A10)"), + // ("ISODD(A1:A10)", "ISODD(A1:A10)"), + // ("ISREF(A1:A10)", "ISREF(A1:A10)"), + // ("ISTEXT(A1:A10)", "ISTEXT(A1:A10)"), + ]; + for (formula, expected) in cases { + let mut t = parser.parse(formula, &cell_reference); + add_implicit_intersection(&mut t, true); + let r = to_string(&t, &cell_reference); + assert_eq!(r, expected); + let excel_formula = to_excel_string(&t, &cell_reference); + assert_eq!(excel_formula, formula); + } +} diff --git a/base/src/expressions/parser/tests/test_implicit_intersection.rs b/base/src/expressions/parser/tests/test_implicit_intersection.rs new file mode 100644 index 00000000..3ee78251 --- /dev/null +++ b/base/src/expressions/parser/tests/test_implicit_intersection.rs @@ -0,0 +1,38 @@ +#![allow(clippy::panic)] + +use crate::expressions::parser::{Node, Parser}; +use crate::expressions::types::CellReferenceRC; +use std::collections::HashMap; + +#[test] +fn simple_tes() { + let worksheets = vec!["Sheet1".to_string()]; + let mut parser = Parser::new(worksheets, HashMap::new()); + + // Reference cell is Sheet1!A1 + let cell_reference = CellReferenceRC { + sheet: "Sheet1".to_string(), + row: 1, + column: 1, + }; + let t = parser.parse("@A1:A10", &Some(cell_reference)); + let child = Node::RangeKind { + sheet_name: None, + sheet_index: 0, + absolute_row1: false, + absolute_column1: false, + row1: 0, + column1: 0, + absolute_row2: false, + absolute_column2: false, + row2: 9, + column2: 0, + }; + assert_eq!( + t, + Node::ImplicitIntersection { + automatic: false, + child: Box::new(child) + } + ) +} diff --git a/base/src/expressions/parser/tests/test_move_formula.rs b/base/src/expressions/parser/tests/test_move_formula.rs index 37fde784..4e36c720 100644 --- a/base/src/expressions/parser/tests/test_move_formula.rs +++ b/base/src/expressions/parser/tests/test_move_formula.rs @@ -387,7 +387,7 @@ fn test_move_formula_misc() { width: 4, height: 5, }; - let node = parser.parse("X9^C2-F4*H2", context); + let node = parser.parse("X9^C2-F4*H2+SUM(F2:H4)+SUM(C2:F6)", context); let t = move_formula( &node, &MoveContext { @@ -400,7 +400,7 @@ fn test_move_formula_misc() { column_delta: 10, }, ); - assert_eq!(t, "X9^M12-P14*H2"); + assert_eq!(t, "X9^M12-P14*H2+SUM(F2:H4)+SUM(M12:P16)"); let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context); let t = move_formula( @@ -475,3 +475,77 @@ fn test_move_formula_another_sheet() { "Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)" ); } + +#[test] +fn move_formula_implicit_intersetion() { + // context is E4 + let row = 4; + let column = 5; + let context = &CellReferenceRC { + sheet: "Sheet1".to_string(), + row, + column, + }; + let worksheets = vec!["Sheet1".to_string()]; + let mut parser = Parser::new(worksheets, vec![], HashMap::new()); + + // Area is C2:F6 + let area = &Area { + sheet: 0, + row: 2, + column: 3, + width: 4, + height: 5, + }; + let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)", context); + let t = move_formula( + &node, + &MoveContext { + source_sheet_name: "Sheet1", + row, + column, + area, + target_sheet_name: "Sheet1", + row_delta: 10, + column_delta: 10, + }, + ); + assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)"); +} + +#[test] +fn move_formula_implicit_intersetion_with_ranges() { + // context is E4 + let row = 4; + let column = 5; + let context = &CellReferenceRC { + sheet: "Sheet1".to_string(), + row, + column, + }; + let worksheets = vec!["Sheet1".to_string()]; + let mut parser = Parser::new(worksheets, vec![], HashMap::new()); + + // Area is C2:F6 + let area = &Area { + sheet: 0, + row: 2, + column: 3, + width: 4, + height: 5, + }; + let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)+SUM(@A1, @X9, @$D$5)", context); + let t = move_formula( + &node, + &MoveContext { + source_sheet_name: "Sheet1", + row, + column, + area, + target_sheet_name: "Sheet1", + row_delta: 10, + column_delta: 10, + }, + ); + assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)+SUM(@A1,@X9,@$N$15)"); +} diff --git a/base/src/expressions/parser/tests/test_tables.rs b/base/src/expressions/parser/tests/test_tables.rs index 3addf985..eced95d9 100644 --- a/base/src/expressions/parser/tests/test_tables.rs +++ b/base/src/expressions/parser/tests/test_tables.rs @@ -3,11 +3,10 @@ use std::collections::HashMap; use crate::expressions::parser::stringify::to_string; -use crate::expressions::utils::{number_to_column, parse_reference_a1}; -use crate::types::{Table, TableColumn, TableStyleInfo}; - use crate::expressions::parser::Parser; use crate::expressions::types::CellReferenceRC; +use crate::expressions::utils::{number_to_column, parse_reference_a1}; +use crate::types::{Table, TableColumn, TableStyleInfo}; fn create_test_table( table_name: &str, diff --git a/base/src/expressions/parser/walk.rs b/base/src/expressions/parser/walk.rs deleted file mode 100644 index 746e3790..00000000 --- a/base/src/expressions/parser/walk.rs +++ /dev/null @@ -1,278 +0,0 @@ -use super::{move_formula::ref_is_in_area, Node}; - -use crate::expressions::types::{Area, CellReferenceIndex}; - -pub(crate) fn forward_references( - node: &mut Node, - context: &CellReferenceIndex, - source_area: &Area, - target_sheet: u32, - target_sheet_name: &str, - target_row: i32, - target_column: i32, -) { - match node { - Node::ReferenceKind { - sheet_name, - sheet_index: reference_sheet, - absolute_row, - absolute_column, - row: reference_row, - column: reference_column, - } => { - let reference_row_absolute = if *absolute_row { - *reference_row - } else { - *reference_row + context.row - }; - let reference_column_absolute = if *absolute_column { - *reference_column - } else { - *reference_column + context.column - }; - if ref_is_in_area( - *reference_sheet, - reference_row_absolute, - reference_column_absolute, - source_area, - ) { - if *reference_sheet != target_sheet { - *sheet_name = Some(target_sheet_name.to_string()); - *reference_sheet = target_sheet; - } - *reference_row = target_row + *reference_row - source_area.row; - *reference_column = target_column + *reference_column - source_area.column; - } - } - Node::RangeKind { - sheet_name, - sheet_index, - absolute_row1, - absolute_column1, - row1, - column1, - absolute_row2, - absolute_column2, - row2, - column2, - } => { - let reference_row1 = if *absolute_row1 { - *row1 - } else { - *row1 + context.row - }; - let reference_column1 = if *absolute_column1 { - *column1 - } else { - *column1 + context.column - }; - - let reference_row2 = if *absolute_row2 { - *row2 - } else { - *row2 + context.row - }; - let reference_column2 = if *absolute_column2 { - *column2 - } else { - *column2 + context.column - }; - if ref_is_in_area(*sheet_index, reference_row1, reference_column1, source_area) - && ref_is_in_area(*sheet_index, reference_row2, reference_column2, source_area) - { - if *sheet_index != target_sheet { - *sheet_index = target_sheet; - *sheet_name = Some(target_sheet_name.to_string()); - } - *row1 = target_row + *row1 - source_area.row; - *column1 = target_column + *column1 - source_area.column; - *row2 = target_row + *row2 - source_area.row; - *column2 = target_column + *column2 - source_area.column; - } - } - // Recurse - Node::OpRangeKind { left, right } => { - forward_references( - left, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - forward_references( - right, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - } - Node::OpConcatenateKind { left, right } => { - forward_references( - left, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - forward_references( - right, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - } - Node::OpSumKind { - kind: _, - left, - right, - } => { - forward_references( - left, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - forward_references( - right, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - } - Node::OpProductKind { - kind: _, - left, - right, - } => { - forward_references( - left, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - forward_references( - right, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - } - Node::OpPowerKind { left, right } => { - forward_references( - left, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - forward_references( - right, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - } - Node::FunctionKind { kind: _, args } => { - for arg in args { - forward_references( - arg, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - } - } - Node::InvalidFunctionKind { name: _, args } => { - for arg in args { - forward_references( - arg, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - } - } - Node::CompareKind { - kind: _, - left, - right, - } => { - forward_references( - left, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - forward_references( - right, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - } - Node::UnaryKind { kind: _, right } => { - forward_references( - right, - context, - source_area, - target_sheet, - target_sheet_name, - target_row, - target_column, - ); - } - // TODO: Not implemented - Node::ArrayKind(_) => {} - // Do nothing. Note: we could do a blanket _ => {} - Node::DefinedNameKind(_) => {} - Node::TableNameKind(_) => {} - Node::WrongVariableKind(_) => {} - Node::ErrorKind(_) => {} - Node::ParseErrorKind { .. } => {} - Node::EmptyArgKind => {} - Node::BooleanKind(_) => {} - Node::NumberKind(_) => {} - Node::StringKind(_) => {} - Node::WrongReferenceKind { .. } => {} - Node::WrongRangeKind { .. } => {} - } -} diff --git a/base/src/expressions/token.rs b/base/src/expressions/token.rs index 877ddc6e..cd981445 100644 --- a/base/src/expressions/token.rs +++ b/base/src/expressions/token.rs @@ -240,6 +240,7 @@ pub enum TokenType { Bang, // ! Percent, // % And, // & + At, // @ Reference { sheet: Option, row: i32, diff --git a/base/src/functions/information.rs b/base/src/functions/information.rs index 44d847ca..5901539f 100644 --- a/base/src/functions/information.rs +++ b/base/src/functions/information.rs @@ -249,7 +249,7 @@ impl Model { // The arg could be a defined name or a table // let = &args[0]; match &args[0] { - Node::DefinedNameKind((name, scope)) => { + Node::DefinedNameKind((name, scope, _)) => { // Let's see if it is a defined name if let Some(defined_name) = self .parsed_defined_names diff --git a/base/src/functions/lookup_and_reference.rs b/base/src/functions/lookup_and_reference.rs index 04a9e956..f8f9735f 100644 --- a/base/src/functions/lookup_and_reference.rs +++ b/base/src/functions/lookup_and_reference.rs @@ -855,7 +855,7 @@ impl Model { if left.row != right.row || left.column != right.column { // FIXME: Implicit intersection or dynamic arrays return CalcResult::Error { - error: Error::ERROR, + error: Error::NIMPL, origin: cell, message: "argument must be a reference to a single cell".to_string(), }; diff --git a/base/src/lib.rs b/base/src/lib.rs index 840a969c..ec25fc99 100644 --- a/base/src/lib.rs +++ b/base/src/lib.rs @@ -41,7 +41,6 @@ pub mod worksheet; mod actions; mod cast; mod constants; -mod diffs; mod functions; mod implicit_intersection; mod model; diff --git a/base/src/model.rs b/base/src/model.rs index 58aa59b5..45a61d24 100644 --- a/base/src/model.rs +++ b/base/src/model.rs @@ -207,6 +207,17 @@ impl Model { }, } } + Node::ImplicitIntersection { + automatic: _, + child, + } => match self.evaluate_node_with_reference(child, cell) { + CalcResult::Range { left, right } => CalcResult::Range { left, right }, + _ => CalcResult::new_error( + Error::ERROR, + cell, + format!("Error with Implicit Intersection in cell {:?}", cell), + ), + }, _ => self.evaluate_node_in_context(node, cell), } } @@ -416,7 +427,7 @@ impl Model { // TODO: NOT IMPLEMENTED CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string()) } - DefinedNameKind((name, scope)) => { + DefinedNameKind((name, scope, _)) => { if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) { match parsed_defined_name { ParsedDefinedName::CellReference(reference) => { @@ -528,6 +539,22 @@ impl Model { format!("Error parsing {}: {}", formula, message), ), EmptyArgKind => CalcResult::EmptyArg, + ImplicitIntersection { + automatic: _, + child, + } => match self.evaluate_node_with_reference(child, cell) { + CalcResult::Range { left, right } => { + match implicit_intersection(&cell, &Range { left, right }) { + Some(cell_reference) => self.evaluate_cell(cell_reference), + None => CalcResult::new_error( + Error::VALUE, + cell, + format!("Error with Implicit Intersection in cell {:?}", cell), + ), + } + } + _ => self.evaluate_node_in_context(child, cell), + }, } } @@ -617,12 +644,15 @@ impl Model { }; } CalcResult::Range { left, right } => { - let range = Range { - left: *left, - right: *right, - }; - if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range) + if left.sheet == right.sheet + && left.row == right.row + && left.column == right.column { + let intersection_cell = CellReferenceIndex { + sheet: left.sheet, + column: left.column, + row: left.row, + }; let v = self.evaluate_cell(intersection_cell); self.set_cell_value(cell_reference, &v); } else { @@ -639,10 +669,32 @@ impl Model { f, s, o, - m: "Invalid reference".to_string(), - ei: Error::VALUE, + m: "Implicit Intersection not implemented".to_string(), + ei: Error::NIMPL, }; } + // if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range) + // { + // let v = self.evaluate_cell(intersection_cell); + // self.set_cell_value(cell_reference, &v); + // } else { + // let o = match self.cell_reference_to_string(&cell_reference) { + // Ok(s) => s, + // Err(_) => "".to_string(), + // }; + // *self.workbook.worksheets[sheet as usize] + // .sheet_data + // .get_mut(&row) + // .expect("expected a row") + // .get_mut(&column) + // .expect("expected a column") = Cell::CellFormulaError { + // f, + // s, + // o, + // m: "Invalid reference".to_string(), + // ei: Error::VALUE, + // }; + // } } CalcResult::EmptyCell | CalcResult::EmptyArg => { *self.workbook.worksheets[sheet as usize] @@ -865,11 +917,7 @@ impl Model { 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(); + let defined_names = workbook.get_defined_names_with_scope(); // add all tables // let mut tables = Vec::new(); // for worksheet in worksheets { diff --git a/base/src/new_empty.rs b/base/src/new_empty.rs index 5399e230..a48c65df 100644 --- a/base/src/new_empty.rs +++ b/base/src/new_empty.rs @@ -144,12 +144,7 @@ impl Model { /// Reparses all formulas and defined names pub(crate) fn reset_parsed_structures(&mut self) { - let defined_names = self - .workbook - .get_defined_names_with_scope() - .iter() - .map(|s| (s.0.to_owned(), s.1)) - .collect(); + let defined_names = self.workbook.get_defined_names_with_scope(); self.parser .set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names); self.parsed_formulas = vec![]; diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 80d1c2b6..2a634af7 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -28,7 +28,6 @@ mod test_fn_sumifs; mod test_fn_textbefore; mod test_fn_textjoin; mod test_fn_unicode; -mod test_forward_references; mod test_frozen_rows_columns; mod test_general; mod test_math; @@ -58,6 +57,7 @@ mod test_fn_type; mod test_frozen_rows_and_columns; mod test_geomean; mod test_get_cell_content; +mod test_implicit_intersection; mod test_issue_155; mod test_percentage; mod test_set_functions_error_handling; diff --git a/base/src/test/test_fn_concatenate.rs b/base/src/test/test_fn_concatenate.rs index 727fe955..dcb2c0a9 100644 --- a/base/src/test/test_fn_concatenate.rs +++ b/base/src/test/test_fn_concatenate.rs @@ -22,13 +22,14 @@ fn fn_concatenate() { model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#); // This will break once we implement the implicit intersection operator // It should be: - // model._set("B2", r#"=CONCATENATE(@A1:A3, "!")"#); + model._set("C2", r#"=CONCATENATE(@A1:A3, "!")"#); model._set("B2", r#"=CONCATENATE(A1:A3, "!")"#); model._set("B3", r#"=CONCAT(A1:A3, "!")"#); model.evaluate(); assert_eq!(model._get_text("B1"), *"Hello my World!"); - assert_eq!(model._get_text("B2"), *" my !"); + assert_eq!(model._get_text("B2"), *"#N/IMPL!"); assert_eq!(model._get_text("B3"), *"Hello my World!"); + assert_eq!(model._get_text("C2"), *" my !"); } diff --git a/base/src/test/test_fn_formulatext.rs b/base/src/test/test_fn_formulatext.rs index bfe338d4..b15180e3 100644 --- a/base/src/test/test_fn_formulatext.rs +++ b/base/src/test/test_fn_formulatext.rs @@ -30,8 +30,18 @@ fn implicit_intersection() { model._set("A2", "=FORMULATEXT(D1:E1)"); model.evaluate(); - assert_eq!(model._get_text("A1"), *"#ERROR!"); - assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A1"), *"#N/IMPL!"); + assert_eq!(model._get_text("A2"), *"#N/IMPL!"); +} + +#[test] +fn implicit_intersection_operator() { + let mut model = new_empty_model(); + model._set("A1", "=1 + 2"); + model._set("B1", "=FORMULATEXT(@A:A)"); + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"#N/IMPL!"); } #[test] diff --git a/base/src/test/test_forward_references.rs b/base/src/test/test_forward_references.rs deleted file mode 100644 index 0b8aa238..00000000 --- a/base/src/test/test_forward_references.rs +++ /dev/null @@ -1,121 +0,0 @@ -#![allow(clippy::unwrap_used)] - -use crate::expressions::types::{Area, CellReferenceIndex}; -use crate::test::util::new_empty_model; - -#[test] -fn test_forward_references() { - let mut model = new_empty_model(); - - // test single ref changed nd not changed - model._set("H8", "=F6*G9"); - // tests areas - model._set("H9", "=SUM(D4:F6)"); - // absolute coordinates - model._set("H10", "=$F$6"); - // area larger than the source area - model._set("H11", "=SUM(D3:F6)"); - // Test arguments and concat - model._set("H12", "=SUM(F6, D4:F6) & D4"); - // Test range operator. This is syntax error for now. - // model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))"); - // Test operations - model._set("H14", "=-D4+D5*F6/F5"); - - model.evaluate(); - - // Source Area is D4:F6 - let source_area = &Area { - sheet: 0, - row: 4, - column: 4, - width: 3, - height: 3, - }; - - // We paste in B10 - let target_row = 10; - let target_column = 2; - let result = model.forward_references( - source_area, - &CellReferenceIndex { - sheet: 0, - row: target_row, - column: target_column, - }, - ); - assert!(result.is_ok()); - model.evaluate(); - - assert_eq!(model._get_formula("H8"), "=D12*G9"); - assert_eq!(model._get_formula("H9"), "=SUM(B10:D12)"); - assert_eq!(model._get_formula("H10"), "=$D$12"); - - assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)"); - assert_eq!(model._get_formula("H12"), "=SUM(D12,B10:D12)&B10"); - // assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))"); - assert_eq!(model._get_formula("H14"), "=-B10+B11*D12/D11"); -} - -#[test] -fn test_different_sheet() { - let mut model = new_empty_model(); - - // test single ref changed not changed - model._set("H8", "=F6*G9"); - // tests areas - model._set("H9", "=SUM(D4:F6)"); - // absolute coordinates - model._set("H10", "=$F$6"); - // area larger than the source area - model._set("H11", "=SUM(D3:F6)"); - // Test arguments and concat - model._set("H12", "=SUM(F6, D4:F6) & D4"); - // Test range operator. This is syntax error for now. - // model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))"); - // Test operations - model._set("H14", "=-D4+D5*F6/F5"); - - // Adds a new sheet - assert!(model.add_sheet("Sheet2").is_ok()); - - model.evaluate(); - - // Source Area is D4:F6 - let source_area = &Area { - sheet: 0, - row: 4, - column: 4, - width: 3, - height: 3, - }; - - // We paste in Sheet2!B10 - let target_row = 10; - let target_column = 2; - let result = model.forward_references( - source_area, - &CellReferenceIndex { - sheet: 1, - row: target_row, - column: target_column, - }, - ); - assert!(result.is_ok()); - model.evaluate(); - - assert_eq!(model._get_formula("H8"), "=Sheet2!D12*G9"); - assert_eq!(model._get_formula("H9"), "=SUM(Sheet2!B10:D12)"); - assert_eq!(model._get_formula("H10"), "=Sheet2!$D$12"); - - assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)"); - assert_eq!( - model._get_formula("H12"), - "=SUM(Sheet2!D12,Sheet2!B10:D12)&Sheet2!B10" - ); - // assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))"); - assert_eq!( - model._get_formula("H14"), - "=-Sheet2!B10+Sheet2!B11*Sheet2!D12/Sheet2!D11" - ); -} diff --git a/base/src/test/test_implicit_intersection.rs b/base/src/test/test_implicit_intersection.rs new file mode 100644 index 00000000..43a039bc --- /dev/null +++ b/base/src/test/test_implicit_intersection.rs @@ -0,0 +1,50 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn simple_colum() { + let mut model = new_empty_model(); + // We populate cells A1 to A3 + model._set("A1", "1"); + model._set("A2", "2"); + model._set("A3", "3"); + + model._set("C2", "=@A1:A3"); + + model.evaluate(); + + assert_eq!(model._get_text("C2"), "2".to_string()); +} + +#[test] +fn return_of_array_is_n_impl() { + let mut model = new_empty_model(); + // We populate cells A1 to A3 + model._set("A1", "1"); + model._set("A2", "2"); + model._set("A3", "3"); + + model._set("C2", "=A1:A3"); + model._set("D2", "=SUM(SIN(A:A)"); + + model.evaluate(); + + assert_eq!(model._get_text("C2"), "#N/IMPL!".to_string()); + assert_eq!(model._get_text("D2"), "#N/IMPL!".to_string()); +} + +#[test] +fn concat() { + let mut model = new_empty_model(); + model._set("A1", "=CONCAT(@B1:B3)"); + model._set("A2", "=CONCAT(B1:B3)"); + model._set("B1", "Hello"); + model._set("B2", " "); + model._set("B3", "world!"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"Hello"); + assert_eq!(model._get_text("A2"), *"Hello world!"); +} diff --git a/base/src/units.rs b/base/src/units.rs index f3139917..4539f868 100644 --- a/base/src/units.rs +++ b/base/src/units.rs @@ -298,6 +298,7 @@ impl Model { Node::WrongVariableKind(_) => None, Node::CompareKind { .. } => None, Node::OpPowerKind { .. } => None, + Node::ImplicitIntersection { .. } => None, } } diff --git a/base/src/workbook.rs b/base/src/workbook.rs index 841537d7..53ad0ff5 100644 --- a/base/src/workbook.rs +++ b/base/src/workbook.rs @@ -1,6 +1,6 @@ use std::vec::Vec; -use crate::types::*; +use crate::{expressions::parser::DefinedNameS, types::*}; impl Workbook { pub fn get_worksheet_names(&self) -> Vec { @@ -29,7 +29,7 @@ impl Workbook { } /// Returns the a list of defined names in the workbook with their scope - pub fn get_defined_names_with_scope(&self) -> Vec<(String, Option, String)> { + pub fn get_defined_names_with_scope(&self) -> Vec { let sheet_id_index: Vec = self.worksheets.iter().map(|s| s.sheet_id).collect(); let defined_names = self diff --git a/xlsx/src/import/worksheets.rs b/xlsx/src/import/worksheets.rs index 1c450b58..a446660d 100644 --- a/xlsx/src/import/worksheets.rs +++ b/xlsx/src/import/worksheets.rs @@ -1,10 +1,11 @@ #![allow(clippy::unwrap_used)] +use ironcalc_base::expressions::parser::static_analysis::add_implicit_intersection; use std::{collections::HashMap, io::Read, num::ParseIntError}; use ironcalc_base::{ expressions::{ - parser::{stringify::to_rc_format, Parser}, + parser::{stringify::to_rc_format, DefinedNameS, Parser}, token::{get_error_by_english_name, Error}, types::CellReferenceRC, utils::{column_to_number, parse_reference_a1}, @@ -42,7 +43,7 @@ pub(crate) struct Relationship { } impl WorkbookXML { - fn get_defined_names_with_scope(&self) -> Vec<(String, Option)> { + fn get_defined_names_with_scope(&self) -> Vec { let sheet_id_index: Vec = self.worksheets.iter().map(|s| s.sheet_id).collect(); let defined_names = self @@ -58,7 +59,7 @@ impl WorkbookXML { // convert Option to Option .map(|pos| pos as u32); - (dn.name.clone(), index) + (dn.name.clone(), index, dn.formula.clone()) }) .collect::>(); defined_names @@ -304,12 +305,14 @@ fn from_a1_to_rc( worksheets: &[String], context: String, tables: HashMap, - defined_names: Vec<(String, Option)>, + defined_names: Vec, ) -> Result { let mut parser = Parser::new(worksheets.to_owned(), defined_names, tables); let cell_reference = parse_reference(&context).map_err(|error| XlsxError::Xml(error.to_string()))?; - let t = parser.parse(&formula, &cell_reference); + let mut t = parser.parse(&formula, &cell_reference); + add_implicit_intersection(&mut t, true); + Ok(to_rc_format(&t)) } @@ -706,7 +709,7 @@ pub(super) fn load_sheet( worksheets: &[String], tables: &HashMap, shared_strings: &mut Vec, - defined_names: Vec<(String, Option)>, + defined_names: Vec, ) -> Result<(Worksheet, bool), XlsxError> { let sheet_name = &settings.name; let sheet_id = settings.id; diff --git a/xlsx/tests/test.rs b/xlsx/tests/test.rs index 34cb4877..c0e6b7b8 100644 --- a/xlsx/tests/test.rs +++ b/xlsx/tests/test.rs @@ -48,8 +48,8 @@ fn test_example() { assert_eq!(ws[0].views[&0].range, [13, 5, 20, 14]); let model2 = load_from_icalc("tests/example.ic").unwrap(); - let s = bitcode::encode(&model2.workbook); - assert_eq!(workbook, model2.workbook, "{:?}", s); + let _ = bitcode::encode(&model2.workbook); + assert_eq!(workbook, model2.workbook); } #[test] @@ -346,21 +346,31 @@ fn test_xlsx() { let path = format!("{}", Uuid::new_v4()); let dir = temp_folder.join(path); fs::create_dir(&dir).unwrap(); + let mut is_error = false; for file_path in entries { let file_name_str = file_path.file_name().unwrap().to_str().unwrap(); let file_path_str = file_path.to_str().unwrap(); println!("Testing file: {}", file_path_str); if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') { if let Err(message) = test_file(file_path_str) { + println!("Error with file: '{file_path_str}'"); println!("{}", message); - panic!("Model was evaluated inconsistently with XLSX data.") + is_error = true; + } + let t = test_load_and_saving(file_path_str, &dir); + if t.is_err() { + println!("Error while load and saving file: {file_path_str}"); + is_error = true; } - assert!(test_load_and_saving(file_path_str, &dir).is_ok()); } else { println!("skipping"); } } fs::remove_dir_all(&dir).unwrap(); + assert!( + !is_error, + "Models were evaluated inconsistently with XLSX data." + ); } #[test] @@ -375,20 +385,26 @@ fn no_export() { let path = format!("{}", Uuid::new_v4()); let dir = temp_folder.join(path); fs::create_dir(&dir).unwrap(); + let mut is_error = false; for file_path in entries { let file_name_str = file_path.file_name().unwrap().to_str().unwrap(); let file_path_str = file_path.to_str().unwrap(); println!("Testing file: {}", file_path_str); if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') { if let Err(message) = test_file(file_path_str) { + println!("Error with file: '{file_path_str}'"); println!("{}", message); - panic!("Model was evaluated inconsistently with XLSX data.") + is_error = true; } } else { println!("skipping"); } } fs::remove_dir_all(&dir).unwrap(); + assert!( + !is_error, + "Models were evaluated inconsistently with XLSX data." + ); } // This test verifies whether exporting the merged cells functionality is happening properly or not. @@ -476,6 +492,7 @@ fn test_documentation_xlsx() { let path = format!("{}", Uuid::new_v4()); let dir = temp_folder.join(path); fs::create_dir(&dir).unwrap(); + let mut is_error = false; for file_path in entries { let file_name_str = file_path.file_name().unwrap().to_str().unwrap(); let file_path_str = file_path.to_str().unwrap(); @@ -487,7 +504,7 @@ fn test_documentation_xlsx() { if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') { if let Err(message) = test_file(file_path_str) { println!("{}", message); - panic!("Model was evaluated inconsistently with XLSX data.") + is_error = true; } assert!(test_load_and_saving(file_path_str, &dir).is_ok()); } else { @@ -495,6 +512,10 @@ fn test_documentation_xlsx() { } } fs::remove_dir_all(&dir).unwrap(); + assert!( + !is_error, + "Models were evaluated inconsistently with XLSX data." + ) } #[test]