From e5ec75495aad5e6dba89c351e3e75d223b7b74c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Thu, 16 Jan 2025 23:44:55 +0100 Subject: [PATCH] UPDATE: Introducing Arrays # This PR introduces: ## Parsing arrays: {1,2,3} and {1;2;3} Note that array elements can be numbers, booleans and errors (#VALUE!) ## Evaluating arrays in the SUM function =SUM({1,2,3}) works! ## Evaluating arithmetic operation with arrays =SUM({1,2,3} * 8) or =SUM({1,2,3}+{2,4,5}) works This is done with just one function (handle_arithmetic) for most operations ## Some mathematical functions implement arrays =SUM(SIN({1,2,3})) works This is done with macros. See fn_single_number So that implementing new functions that supports array are easy # Not done in this PR ## Most functions are not supporting arrays When that happens we either through #N/IMPL! (not implemented error) or do implicit intersection. Some functions will be rather trivial to "arraify" some will be hard ## The final result in a cell cannot be an array The formula ={1,2,3} in a cell will result in #N/IMPL! ## Exporting arrays to Excel might not work correctly Excel uses the cm (cell metadata) for formulas that contain dynamic arrays. Although the present PR does not introduce dynamic arrays some formulas like =SUM(SIN({1,2,3})) is considered a dynamic formula ## There are not a lot of tests in this delivery The bulk of the tests will be added once we start going function by function# This PR introduces: ## Parsing arrays: {1,2,3} and {1;2;3} Note that array elements can be numbers, booleans and errors (#VALUE!) ## Evaluating arrays in the SUM function =SUM({1,2,3}) works! ## Evaluating arithmetic operation with arrays =SUM({1,2,3} * 8) or =SUM({1,2,3}+{2,4,5}) works This is done with just one function (handle_arithmetic) for most operations ## Some mathematical functions implement arrays =SUM(SIN({1,2,3})) works This is done with macros. See fn_single_number So that implementing new functions that supports array are easy # Not done in this PR ## Most functions are not supporting arrays When that happens we either through #N/IMPL! (not implemented error) or do implicit intersection. Some functions will be rather trivial to "arraify" some will be hard ## The final result in a cell cannot be an array The formula ={1,2,3} in a cell will result in #N/IMPL! ## Exporting arrays to Excel might not work correctly Excel uses the cm (cell metadata) for formulas that contain dynamic arrays. Although the present PR does not introduce dynamic arrays some formulas like =SUM(SIN({1,2,3})) is considered a dynamic formula ## There are not a lot of tests in this delivery The bulk of the tests will be added once we start going function by function ## The array parsing does not respect the locale Locales that use ',' as a decimal separator need to use something different for arrays ## The might introduce a small performance penalty We haven't been benchmarking, and having closures for every arithmetic operation and every function evaluation will introduce a performance hit. Fixing that in he future is not so hard writing tailored code for the operation --- base/src/arithmetic.rs | 158 ++++++++++ base/src/calc_result.rs | 3 +- base/src/cast.rs | 92 +++++- base/src/expressions/parser/mod.rs | 89 +++++- base/src/expressions/parser/move_formula.rs | 50 +++- base/src/expressions/parser/stringify.rs | 57 +++- base/src/expressions/parser/tests/mod.rs | 1 + .../expressions/parser/tests/test_arrays.rs | 92 ++++++ base/src/functions/information.rs | 5 + base/src/functions/logical.rs | 14 + base/src/functions/macros.rs | 100 +++++++ base/src/functions/mathematical.rs | 275 +++--------------- base/src/functions/mod.rs | 1 + base/src/functions/statistical.rs | 14 + base/src/functions/subtotal.rs | 14 + base/src/functions/text.rs | 117 ++++++++ base/src/functions/util.rs | 6 +- base/src/lib.rs | 1 + base/src/model.rs | 97 ++---- base/src/test/mod.rs | 1 + base/src/test/test_arrays.rs | 13 + base/src/test/test_fn_sum.rs | 16 + base/src/test/test_implicit_intersection.rs | 2 +- .../frontend/package-lock.json | 1 + webapp/app.ironcalc.com/server/Cargo.lock | 6 +- xlsx/src/import/worksheets.rs | 18 +- xlsx/tests/calc_tests/simple_arrays.xlsx | Bin 0 -> 9494 bytes 27 files changed, 899 insertions(+), 344 deletions(-) create mode 100644 base/src/arithmetic.rs create mode 100644 base/src/expressions/parser/tests/test_arrays.rs create mode 100644 base/src/functions/macros.rs create mode 100644 base/src/test/test_arrays.rs create mode 100644 xlsx/tests/calc_tests/simple_arrays.xlsx diff --git a/base/src/arithmetic.rs b/base/src/arithmetic.rs new file mode 100644 index 0000000..26f729d --- /dev/null +++ b/base/src/arithmetic.rs @@ -0,0 +1,158 @@ +use crate::{ + calc_result::CalcResult, + cast::NumberOrArray, + expressions::{ + parser::{ArrayNode, Node}, + token::Error, + types::CellReferenceIndex, + }, + model::Model, +}; + +/// Unify how we map booleans/strings to f64 +fn to_f64(value: &ArrayNode) -> Result { + match value { + ArrayNode::Number(f) => Ok(*f), + ArrayNode::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }), + ArrayNode::String(s) => match s.parse::() { + Ok(f) => Ok(f), + Err(_) => Err(Error::VALUE), + }, + ArrayNode::Error(err) => Err(err.clone()), + } +} + +impl Model { + /// Applies `op` element‐wise for arrays/numbers. + pub(crate) fn handle_arithmetic( + &mut self, + left: &Node, + right: &Node, + cell: CellReferenceIndex, + op: &dyn Fn(f64, f64) -> Result, + ) -> CalcResult { + let l = match self.get_number_or_array(left, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + let r = match self.get_number_or_array(right, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + match (l, r) { + // ----------------------------------------------------- + // Case 1: Both are numbers + // ----------------------------------------------------- + (NumberOrArray::Number(f1), NumberOrArray::Number(f2)) => match op(f1, f2) { + Ok(x) => CalcResult::Number(x), + Err(Error::DIV) => CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Divide by 0".to_string(), + }, + Err(Error::VALUE) => CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid number".to_string(), + }, + Err(e) => CalcResult::Error { + error: e, + origin: cell, + message: "Unknown error".to_string(), + }, + }, + + // ----------------------------------------------------- + // Case 2: left is Number, right is Array + // ----------------------------------------------------- + (NumberOrArray::Number(f1), NumberOrArray::Array(a2)) => { + let mut array = Vec::new(); + for row in a2 { + let mut data_row = Vec::new(); + for node in row { + match to_f64(&node) { + Ok(f2) => match op(f1, f2) { + Ok(x) => data_row.push(ArrayNode::Number(x)), + Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)), + Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)), + Err(e) => data_row.push(ArrayNode::Error(e)), + }, + Err(err) => data_row.push(ArrayNode::Error(err)), + } + } + array.push(data_row); + } + CalcResult::Array(array) + } + + // ----------------------------------------------------- + // Case 3: left is Array, right is Number + // ----------------------------------------------------- + (NumberOrArray::Array(a1), NumberOrArray::Number(f2)) => { + let mut array = Vec::new(); + for row in a1 { + let mut data_row = Vec::new(); + for node in row { + match to_f64(&node) { + Ok(f1) => match op(f1, f2) { + Ok(x) => data_row.push(ArrayNode::Number(x)), + Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)), + Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)), + Err(e) => data_row.push(ArrayNode::Error(e)), + }, + Err(err) => data_row.push(ArrayNode::Error(err)), + } + } + array.push(data_row); + } + CalcResult::Array(array) + } + + // ----------------------------------------------------- + // Case 4: Both are arrays + // ----------------------------------------------------- + (NumberOrArray::Array(a1), NumberOrArray::Array(a2)) => { + let n1 = a1.len(); + let m1 = a1.first().map(|r| r.len()).unwrap_or(0); + let n2 = a2.len(); + let m2 = a2.first().map(|r| r.len()).unwrap_or(0); + let n = n1.max(n2); + let m = m1.max(m2); + + let mut array = Vec::new(); + for i in 0..n { + let row1 = a1.get(i); + let row2 = a2.get(i); + + let mut data_row = Vec::new(); + for j in 0..m { + let val1 = row1.and_then(|r| r.get(j)); + let val2 = row2.and_then(|r| r.get(j)); + + match (val1, val2) { + (Some(v1), Some(v2)) => match (to_f64(v1), to_f64(v2)) { + (Ok(f1), Ok(f2)) => match op(f1, f2) { + Ok(x) => data_row.push(ArrayNode::Number(x)), + Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)), + Err(Error::VALUE) => { + data_row.push(ArrayNode::Error(Error::VALUE)) + } + Err(e) => data_row.push(ArrayNode::Error(e)), + }, + (Err(e), _) | (_, Err(e)) => data_row.push(ArrayNode::Error(e)), + }, + // Mismatched dimensions => #VALUE! + _ => data_row.push(ArrayNode::Error(Error::VALUE)), + } + } + array.push(data_row); + } + CalcResult::Array(array) + } + } + } +} diff --git a/base/src/calc_result.rs b/base/src/calc_result.rs index a1db54a..1691a82 100644 --- a/base/src/calc_result.rs +++ b/base/src/calc_result.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use crate::expressions::{token::Error, types::CellReferenceIndex}; +use crate::expressions::{parser::ArrayNode, token::Error, types::CellReferenceIndex}; #[derive(Clone)] pub struct Range { @@ -24,6 +24,7 @@ pub(crate) enum CalcResult { }, EmptyCell, EmptyArg, + Array(Vec>), } impl CalcResult { diff --git a/base/src/cast.rs b/base/src/cast.rs index afc6338..0ff8db5 100644 --- a/base/src/cast.rs +++ b/base/src/cast.rs @@ -1,10 +1,85 @@ use crate::{ calc_result::{CalcResult, Range}, - expressions::{parser::Node, token::Error, types::CellReferenceIndex}, + expressions::{ + parser::{ArrayNode, Node}, + token::Error, + types::CellReferenceIndex, + }, model::Model, }; +pub(crate) enum NumberOrArray { + Number(f64), + Array(Vec>), +} + impl Model { + pub(crate) fn get_number_or_array( + &mut self, + node: &Node, + cell: CellReferenceIndex, + ) -> Result { + match self.evaluate_node_in_context(node, cell) { + CalcResult::Number(f) => Ok(NumberOrArray::Number(f)), + CalcResult::String(s) => match s.parse::() { + Ok(f) => Ok(NumberOrArray::Number(f)), + _ => Err(CalcResult::new_error( + Error::VALUE, + cell, + "Expecting number".to_string(), + )), + }, + CalcResult::Boolean(f) => { + if f { + Ok(NumberOrArray::Number(1.0)) + } else { + Ok(NumberOrArray::Number(0.0)) + } + } + CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(NumberOrArray::Number(0.0)), + CalcResult::Range { left, right } => { + let sheet = left.sheet; + if sheet != right.sheet { + return Err(CalcResult::Error { + error: Error::ERROR, + origin: cell, + message: "3D ranges are not allowed".to_string(), + }); + } + // we need to convert the range into an array + let mut array = Vec::new(); + for row in left.row..=right.row { + let mut row_data = Vec::new(); + for column in left.column..=right.column { + let value = + match self.evaluate_cell(CellReferenceIndex { sheet, column, row }) { + CalcResult::String(s) => ArrayNode::String(s), + CalcResult::Number(f) => ArrayNode::Number(f), + CalcResult::Boolean(b) => ArrayNode::Boolean(b), + CalcResult::Error { error, .. } => ArrayNode::Error(error), + CalcResult::Range { .. } => { + // if we do things right this can never happen. + // the evaluation of a cell should never return a range + ArrayNode::Number(0.0) + } + CalcResult::EmptyCell => ArrayNode::Number(0.0), + CalcResult::EmptyArg => ArrayNode::Number(0.0), + CalcResult::Array(_) => { + // if we do things right this can never happen. + // the evaluation of a cell should never return an array + ArrayNode::Number(0.0) + } + }; + row_data.push(value); + } + array.push(row_data); + } + Ok(NumberOrArray::Array(array)) + } + CalcResult::Array(s) => Ok(NumberOrArray::Array(s)), + error @ CalcResult::Error { .. } => Err(error), + } + } pub(crate) fn get_number( &mut self, node: &Node, @@ -43,6 +118,11 @@ impl Model { origin: cell, message: "Arrays not supported yet".to_string(), }), + CalcResult::Array(_) => Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }), } } @@ -95,6 +175,11 @@ impl Model { origin: cell, message: "Arrays not supported yet".to_string(), }), + CalcResult::Array(_) => Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }), } } @@ -139,6 +224,11 @@ impl Model { origin: cell, message: "Arrays not supported yet".to_string(), }), + CalcResult::Array(_) => Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }), } } diff --git a/base/src/expressions/parser/mod.rs b/base/src/expressions/parser/mod.rs index f284fb9..4d655e3 100644 --- a/base/src/expressions/parser/mod.rs +++ b/base/src/expressions/parser/mod.rs @@ -94,6 +94,14 @@ pub(crate) struct Reference<'a> { column: i32, } +#[derive(PartialEq, Clone, Debug)] +pub enum ArrayNode { + Boolean(bool), + Number(f64), + String(String), + Error(token::Error), +} + #[derive(PartialEq, Clone, Debug)] pub enum Node { BooleanKind(bool), @@ -167,7 +175,7 @@ pub enum Node { name: String, args: Vec, }, - ArrayKind(Vec), + ArrayKind(Vec>), DefinedNameKind(DefinedNameS), TableNameKind(String), WrongVariableKind(String), @@ -454,6 +462,49 @@ impl Parser { self.parse_primary() } + fn parse_array_row(&mut self) -> Result, Node> { + let mut row = Vec::new(); + // and array can only have numbers, string or booleans + // otherwise it is a syntax error + let first_element = match self.parse_expr() { + Node::BooleanKind(s) => ArrayNode::Boolean(s), + Node::NumberKind(s) => ArrayNode::Number(s), + Node::StringKind(s) => ArrayNode::String(s), + Node::ErrorKind(kind) => ArrayNode::Error(kind), + error @ Node::ParseErrorKind { .. } => return Err(error), + _ => { + return Err(Node::ParseErrorKind { + formula: self.lexer.get_formula(), + message: "Invalid value in array".to_string(), + position: self.lexer.get_position() as usize, + }); + } + }; + row.push(first_element); + let mut next_token = self.lexer.peek_token(); + // FIXME: this is not respecting the locale + while next_token == TokenType::Comma { + self.lexer.advance_token(); + let value = match self.parse_expr() { + Node::BooleanKind(s) => ArrayNode::Boolean(s), + Node::NumberKind(s) => ArrayNode::Number(s), + Node::StringKind(s) => ArrayNode::String(s), + Node::ErrorKind(kind) => ArrayNode::Error(kind), + error @ Node::ParseErrorKind { .. } => return Err(error), + _ => { + return Err(Node::ParseErrorKind { + formula: self.lexer.get_formula(), + message: "Invalid value in array".to_string(), + position: self.lexer.get_position() as usize, + }); + } + }; + row.push(value); + next_token = self.lexer.peek_token(); + } + Ok(row) + } + fn parse_primary(&mut self) -> Node { let next_token = self.lexer.next_token(); match next_token { @@ -475,21 +526,35 @@ impl Parser { TokenType::Number(s) => Node::NumberKind(s), TokenType::String(s) => Node::StringKind(s), TokenType::LeftBrace => { - let t = self.parse_expr(); - if let Node::ParseErrorKind { .. } = t { - return t; - } + // It's an array. It's a collection of rows all of the same dimension + + let first_row = match self.parse_array_row() { + Ok(s) => s, + Err(error) => return error, + }; + let length = first_row.len(); + + let mut matrix = Vec::new(); + matrix.push(first_row); + // FIXME: this is not respecting the locale let mut next_token = self.lexer.peek_token(); - let mut args: Vec = vec![t]; while next_token == TokenType::Semicolon { self.lexer.advance_token(); - let p = self.parse_expr(); - if let Node::ParseErrorKind { .. } = p { - return p; - } + let row = match self.parse_array_row() { + Ok(s) => s, + Err(error) => return error, + }; next_token = self.lexer.peek_token(); - args.push(p); + if row.len() != length { + return Node::ParseErrorKind { + formula: self.lexer.get_formula(), + position: self.lexer.get_position() as usize, + message: "All rows in an array should be the same length".to_string(), + }; + } + matrix.push(row); } + if let Err(err) = self.lexer.expect(TokenType::RightBrace) { return Node::ParseErrorKind { formula: self.lexer.get_formula(), @@ -497,7 +562,7 @@ impl Parser { message: err.message, }; } - Node::ArrayKind(args) + Node::ArrayKind(matrix) } TokenType::Reference { sheet, diff --git a/base/src/expressions/parser/move_formula.rs b/base/src/expressions/parser/move_formula.rs index 6b2bb1f..956f742 100644 --- a/base/src/expressions/parser/move_formula.rs +++ b/base/src/expressions/parser/move_formula.rs @@ -1,6 +1,6 @@ use super::{ stringify::{stringify_reference, DisplaceData}, - Node, Reference, + ArrayNode, Node, Reference, }; use crate::{ constants::{LAST_COLUMN, LAST_ROW}, @@ -56,6 +56,15 @@ fn move_function(name: &str, args: &Vec, move_context: &MoveContext) -> St format!("{}({})", name, arguments) } +pub(crate) fn to_string_array_node(node: &ArrayNode) -> String { + match node { + ArrayNode::Boolean(value) => format!("{}", value).to_ascii_uppercase(), + ArrayNode::Number(number) => to_excel_precision_str(*number), + ArrayNode::String(value) => format!("\"{}\"", value), + ArrayNode::Error(kind) => format!("{}", kind), + } +} + fn to_string_moved(node: &Node, move_context: &MoveContext) -> String { use self::Node::*; match node { @@ -362,18 +371,39 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String { move_function(name, args, move_context) } ArrayKind(args) => { - // This code is a placeholder. Arrays are not yet implemented - let mut first = true; - let mut arguments = "".to_string(); - for el in args { - if !first { - arguments = format!("{},{}", arguments, to_string_moved(el, move_context)); + let mut first_row = true; + let mut matrix_string = String::new(); + + // Each element in `args` is assumed to be one "row" (itself a `Vec`). + for row in args { + if !first_row { + matrix_string.push(','); } else { - first = false; - arguments = to_string_moved(el, move_context); + first_row = false; } + + // Build the string for the current row + let mut first_col = true; + let mut row_string = String::new(); + for el in row { + if !first_col { + row_string.push(','); + } else { + first_col = false; + } + + // Reuse your existing element-stringification function + row_string.push_str(&to_string_array_node(el)); + } + + // Enclose the row in braces + matrix_string.push('{'); + matrix_string.push_str(&row_string); + matrix_string.push('}'); } - format!("{{{}}}", arguments) + + // Enclose the whole matrix in braces + format!("{{{}}}", matrix_string) } DefinedNameKind((name, ..)) => name.to_string(), TableNameKind(name) => name.to_string(), diff --git a/base/src/expressions/parser/stringify.rs b/base/src/expressions/parser/stringify.rs index 0b87865..b2b7958 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::move_formula::to_string_array_node; 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}; @@ -258,6 +259,31 @@ fn format_function( format!("{}({})", name, arguments) } +// There is just one representation in the AST (Abstract Syntax Tree) of a formula. +// But three different ways to convert it to a string. +// +// To stringify a formula we need a "context", that is in which cell are we doing the "stringifying" +// +// But there are three ways to stringify a formula: +// +// * To show it to the IronCalc user +// * To store internally +// * To export to Excel +// +// There are, of course correspondingly three "modes" when parsing a formula. +// +// The internal representation is the more different as references are stored in the RC representation. +// The the AST of the formula is kept close to this representation we don't need a context +// +// In the export to Excel representation certain things are different: +// * We add a _xlfn. in front of some (more modern) functions +// * We remove the Implicit Intersection operator when it is automatic and add _xlfn.SINGLE when it is not +// +// Examples: +// * =A1+B2 +// * =RC+R1C1 +// * =A1+B1 + fn stringify( node: &Node, context: Option<&CellReferenceRC>, @@ -535,21 +561,28 @@ fn stringify( format_function(&name, args, context, displace_data, export_to_excel) } ArrayKind(args) => { - let mut first = true; - let mut arguments = "".to_string(); - for el in args { - if !first { - arguments = format!( - "{},{}", - arguments, - stringify(el, context, displace_data, export_to_excel) - ); + let mut first_row = true; + let mut matrix_string = String::new(); + + for row in args { + if !first_row { + matrix_string.push(';'); } else { - first = false; - arguments = stringify(el, context, displace_data, export_to_excel); + first_row = false; } + let mut first_column = true; + let mut row_string = String::new(); + for el in row { + if !first_column { + row_string.push(','); + } else { + first_column = false; + } + row_string.push_str(&to_string_array_node(el)); + } + matrix_string.push_str(&row_string); } - format!("{{{}}}", arguments) + format!("{{{}}}", matrix_string) } TableNameKind(value) => value.to_string(), DefinedNameKind((name, ..)) => name.to_string(), diff --git a/base/src/expressions/parser/tests/mod.rs b/base/src/expressions/parser/tests/mod.rs index 009514a..e019d01 100644 --- a/base/src/expressions/parser/tests/mod.rs +++ b/base/src/expressions/parser/tests/mod.rs @@ -1,4 +1,5 @@ mod test_add_implicit_intersection; +mod test_arrays; mod test_general; mod test_implicit_intersection; mod test_issue_155; diff --git a/base/src/expressions/parser/tests/test_arrays.rs b/base/src/expressions/parser/tests/test_arrays.rs new file mode 100644 index 0000000..28b5bf0 --- /dev/null +++ b/base/src/expressions/parser/tests/test_arrays.rs @@ -0,0 +1,92 @@ +#![allow(clippy::panic)] + +use std::collections::HashMap; + +use crate::expressions::parser::stringify::{to_rc_format, to_string}; +use crate::expressions::parser::{ArrayNode, Node, Parser}; +use crate::expressions::types::CellReferenceRC; + +#[test] +fn simple_horizontal() { + 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 horizontal = parser.parse("{1, 2, 3}", &cell_reference); + assert_eq!( + horizontal, + Node::ArrayKind(vec![vec![ + ArrayNode::Number(1.0), + ArrayNode::Number(2.0), + ArrayNode::Number(3.0) + ]]) + ); + + assert_eq!(to_rc_format(&horizontal), "{1,2,3}"); + assert_eq!(to_string(&horizontal, &cell_reference), "{1,2,3}"); +} + +#[test] +fn simple_vertical() { + 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 vertical = parser.parse("{1;2; 3}", &cell_reference); + assert_eq!( + vertical, + Node::ArrayKind(vec![ + vec![ArrayNode::Number(1.0)], + vec![ArrayNode::Number(2.0)], + vec![ArrayNode::Number(3.0)] + ]) + ); + assert_eq!(to_rc_format(&vertical), "{1;2;3}"); + assert_eq!(to_string(&vertical, &cell_reference), "{1;2;3}"); +} + +#[test] +fn simple_matrix() { + 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 matrix = parser.parse("{1,2,3; 4, 5, 6; 7,8,9}", &cell_reference); + assert_eq!( + matrix, + Node::ArrayKind(vec![ + vec![ + ArrayNode::Number(1.0), + ArrayNode::Number(2.0), + ArrayNode::Number(3.0) + ], + vec![ + ArrayNode::Number(4.0), + ArrayNode::Number(5.0), + ArrayNode::Number(6.0) + ], + vec![ + ArrayNode::Number(7.0), + ArrayNode::Number(8.0), + ArrayNode::Number(9.0) + ] + ]) + ); + assert_eq!(to_rc_format(&matrix), "{1,2,3;4,5,6;7,8,9}"); + assert_eq!(to_string(&matrix, &cell_reference), "{1,2,3;4,5,6;7,8,9}"); +} diff --git a/base/src/functions/information.rs b/base/src/functions/information.rs index 5901539..8121c65 100644 --- a/base/src/functions/information.rs +++ b/base/src/functions/information.rs @@ -235,6 +235,11 @@ impl Model { // This cannot happen CalcResult::Number(1.0) } + CalcResult::Array(_) => CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }, } } pub(crate) fn fn_sheet(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { diff --git a/base/src/functions/logical.rs b/base/src/functions/logical.rs index a23e91e..f655f8f 100644 --- a/base/src/functions/logical.rs +++ b/base/src/functions/logical.rs @@ -161,6 +161,13 @@ impl Model { CalcResult::Range { .. } | CalcResult::String { .. } | CalcResult::EmptyCell => {} + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } } if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value) @@ -185,6 +192,13 @@ impl Model { } // References to empty cells are ignored. If all args are ignored the result is #VALUE! CalcResult::EmptyCell => {} + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } } if let (Some(current_result), Some(short_circuit_value)) = (result, short_circuit_value) diff --git a/base/src/functions/macros.rs b/base/src/functions/macros.rs new file mode 100644 index 0000000..378aa18 --- /dev/null +++ b/base/src/functions/macros.rs @@ -0,0 +1,100 @@ +#[macro_export] +macro_rules! single_number_fn { + // The macro takes: + // 1) A function name to define (e.g. fn_sin) + // 2) The operation to apply (e.g. f64::sin) + ($fn_name:ident, $op:expr) => { + pub(crate) fn $fn_name(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + // 1) Check exactly one argument + if args.len() != 1 { + return CalcResult::new_args_number_error(cell); + } + // 2) Try to get a "NumberOrArray" + match self.get_number_or_array(&args[0], cell) { + // ----------------------------------------- + // Case A: It's a single number + // ----------------------------------------- + Ok(NumberOrArray::Number(f)) => match $op(f) { + Ok(x) => CalcResult::Number(x), + Err(Error::DIV) => CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Divide by 0".to_string(), + }, + Err(Error::VALUE) => CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid number".to_string(), + }, + Err(e) => CalcResult::Error { + error: e, + origin: cell, + message: "Unknown error".to_string(), + }, + }, + + // ----------------------------------------- + // Case B: It's an array, so apply $op + // element-by-element. + // ----------------------------------------- + Ok(NumberOrArray::Array(a)) => { + let mut array = Vec::new(); + for row in a { + let mut data_row = Vec::with_capacity(row.len()); + for value in row { + match value { + // If Boolean, treat as 0.0 or 1.0 + ArrayNode::Boolean(b) => { + let n = if b { 1.0 } else { 0.0 }; + match $op(n) { + Ok(x) => data_row.push(ArrayNode::Number(x)), + Err(Error::DIV) => { + data_row.push(ArrayNode::Error(Error::DIV)) + } + Err(Error::VALUE) => { + data_row.push(ArrayNode::Error(Error::VALUE)) + } + Err(e) => data_row.push(ArrayNode::Error(e)), + } + } + // If Number, apply directly + ArrayNode::Number(n) => match $op(n) { + Ok(x) => data_row.push(ArrayNode::Number(x)), + Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)), + Err(Error::VALUE) => { + data_row.push(ArrayNode::Error(Error::VALUE)) + } + Err(e) => data_row.push(ArrayNode::Error(e)), + }, + // If String, parse to f64 then apply or #VALUE! error + ArrayNode::String(s) => { + let node = match s.parse::() { + Ok(f) => match $op(f) { + Ok(x) => ArrayNode::Number(x), + Err(Error::DIV) => ArrayNode::Error(Error::DIV), + Err(Error::VALUE) => ArrayNode::Error(Error::VALUE), + Err(e) => ArrayNode::Error(e), + }, + Err(_) => ArrayNode::Error(Error::VALUE), + }; + data_row.push(node); + } + // If Error, propagate the error + e @ ArrayNode::Error(_) => { + data_row.push(e); + } + } + } + array.push(data_row); + } + CalcResult::Array(array) + } + + // ----------------------------------------- + // Case C: It's an Error => just return it + // ----------------------------------------- + Err(err_result) => err_result, + } + } + }; +} diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 12402c2..82f4b8b 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -1,5 +1,8 @@ +use crate::cast::NumberOrArray; use crate::constants::{LAST_COLUMN, LAST_ROW}; +use crate::expressions::parser::ArrayNode; use crate::expressions::types::CellReferenceIndex; +use crate::single_number_fn; use crate::{ calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, }; @@ -169,6 +172,27 @@ impl Model { } } } + CalcResult::Array(array) => { + for row in array { + for value in row { + match value { + ArrayNode::Number(value) => { + result += value; + } + ArrayNode::Error(error) => { + return CalcResult::Error { + error, + origin: cell, + message: "Error in array".to_string(), + } + } + _ => { + // We ignore booleans and strings + } + } + } + } + } error @ CalcResult::Error { .. } => return error, _ => { // We ignore booleans and strings @@ -354,187 +378,29 @@ impl Model { } } - pub(crate) fn fn_sin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.sin(); - CalcResult::Number(result) - } - pub(crate) fn fn_cos(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.cos(); - CalcResult::Number(result) - } - - pub(crate) fn fn_tan(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.tan(); - CalcResult::Number(result) - } - - pub(crate) fn fn_sinh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.sinh(); - CalcResult::Number(result) - } - pub(crate) fn fn_cosh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.cosh(); - CalcResult::Number(result) - } - - pub(crate) fn fn_tanh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.tanh(); - CalcResult::Number(result) - } - - pub(crate) fn fn_asin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.asin(); - if result.is_nan() || result.is_infinite() { - return CalcResult::Error { - error: Error::NUM, - origin: cell, - message: "Invalid argument for ASIN".to_string(), - }; - } - CalcResult::Number(result) - } - pub(crate) fn fn_acos(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.acos(); - if result.is_nan() || result.is_infinite() { - return CalcResult::Error { - error: Error::NUM, - origin: cell, - message: "Invalid argument for COS".to_string(), - }; - } - CalcResult::Number(result) - } - - pub(crate) fn fn_atan(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.atan(); - if result.is_nan() || result.is_infinite() { - return CalcResult::Error { - error: Error::NUM, - origin: cell, - message: "Invalid argument for ATAN".to_string(), - }; - } - CalcResult::Number(result) - } - - pub(crate) fn fn_asinh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.asinh(); - if result.is_nan() || result.is_infinite() { - return CalcResult::Error { - error: Error::NUM, - origin: cell, - message: "Invalid argument for ASINH".to_string(), - }; - } - CalcResult::Number(result) - } - pub(crate) fn fn_acosh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.acosh(); - if result.is_nan() || result.is_infinite() { - return CalcResult::Error { - error: Error::NUM, - origin: cell, - message: "Invalid argument for ACOSH".to_string(), - }; - } - CalcResult::Number(result) - } - - pub(crate) fn fn_atanh(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - let result = value.atanh(); - if result.is_nan() || result.is_infinite() { - return CalcResult::Error { - error: Error::NUM, - origin: cell, - message: "Invalid argument for ATANH".to_string(), - }; - } - CalcResult::Number(result) - } + single_number_fn!(fn_sin, |f| Ok(f64::sin(f))); + single_number_fn!(fn_cos, |f| Ok(f64::cos(f))); + single_number_fn!(fn_tan, |f| Ok(f64::tan(f))); + single_number_fn!(fn_sinh, |f| Ok(f64::sinh(f))); + single_number_fn!(fn_cosh, |f| Ok(f64::cosh(f))); + single_number_fn!(fn_tanh, |f| Ok(f64::tanh(f))); + single_number_fn!(fn_asin, |f| Ok(f64::asin(f))); + single_number_fn!(fn_acos, |f| Ok(f64::acos(f))); + single_number_fn!(fn_atan, |f| Ok(f64::atan(f))); + single_number_fn!(fn_asinh, |f| Ok(f64::asinh(f))); + single_number_fn!(fn_acosh, |f| Ok(f64::acosh(f))); + single_number_fn!(fn_atanh, |f| Ok(f64::atanh(f))); + single_number_fn!(fn_abs, |f| Ok(f64::abs(f))); + single_number_fn!(fn_sqrt, |f| if f < 0.0 { + Err(Error::NUM) + } else { + Ok(f64::sqrt(f)) + }); + single_number_fn!(fn_sqrtpi, |f: f64| if f < 0.0 { + Err(Error::NUM) + } else { + Ok((f * PI).sqrt()) + }); pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if !args.is_empty() { @@ -543,53 +409,6 @@ impl Model { CalcResult::Number(PI) } - pub(crate) fn fn_abs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - CalcResult::Number(value.abs()) - } - - pub(crate) fn fn_sqrtpi(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - if value < 0.0 { - return CalcResult::Error { - error: Error::NUM, - origin: cell, - message: "Argument of SQRTPI should be >= 0".to_string(), - }; - } - CalcResult::Number((value * PI).sqrt()) - } - - pub(crate) fn fn_sqrt(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 1 { - return CalcResult::new_args_number_error(cell); - } - let value = match self.get_number(&args[0], cell) { - Ok(f) => f, - Err(s) => return s, - }; - if value < 0.0 { - return CalcResult::Error { - error: Error::NUM, - origin: cell, - message: "Argument of SQRT should be >= 0".to_string(), - }; - } - CalcResult::Number(value.sqrt()) - } - pub(crate) fn fn_atan2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if args.len() != 2 { return CalcResult::new_args_number_error(cell); diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 882d44e..fa25560 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -15,6 +15,7 @@ mod financial_util; mod information; mod logical; mod lookup_and_reference; +mod macros; mod mathematical; mod statistical; mod subtotal; diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index 140e6b8..cdb9364 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -134,6 +134,13 @@ impl Model { ); } CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } } } } @@ -165,6 +172,13 @@ impl Model { } error @ CalcResult::Error { .. } => return error, CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; } if count == 0.0 { diff --git a/base/src/functions/subtotal.rs b/base/src/functions/subtotal.rs index d0f9b86..cd3f49b 100644 --- a/base/src/functions/subtotal.rs +++ b/base/src/functions/subtotal.rs @@ -182,6 +182,13 @@ impl Model { } } CalcResult::EmptyCell | CalcResult::EmptyArg => result.push(0.0), + CalcResult::Array(_) => { + return Err(CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }) + } } } } @@ -426,6 +433,13 @@ impl Model { | CalcResult::Number(_) | CalcResult::Boolean(_) | CalcResult::Error { .. } => counta += 1, + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } } } } diff --git a/base/src/functions/text.rs b/base/src/functions/text.rs index 7538e9c..d85db6f 100644 --- a/base/src/functions/text.rs +++ b/base/src/functions/text.rs @@ -97,10 +97,24 @@ impl Model { error @ CalcResult::Error { .. } => return error, CalcResult::EmptyCell | CalcResult::EmptyArg => {} CalcResult::Range { .. } => {} + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } } } } } + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; } CalcResult::String(result) @@ -125,6 +139,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0, + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; let format_code = match self.get_string(&args[1], cell) { Ok(s) => s, @@ -280,6 +301,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; return CalcResult::Number(s.chars().count() as f64); } @@ -308,6 +336,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; return CalcResult::String(s.trim().to_owned()); } @@ -336,6 +371,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; return CalcResult::String(s.to_lowercase()); } @@ -370,6 +412,13 @@ impl Model { message: "Empty cell".to_string(), } } + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; match s.chars().next() { @@ -411,6 +460,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; return CalcResult::String(s.to_uppercase()); } @@ -441,6 +497,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; let num_chars = if args.len() == 2 { match self.evaluate_node_in_context(&args[1], cell) { @@ -471,6 +534,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => 0, + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } } } else { 1 @@ -509,6 +579,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; let num_chars = if args.len() == 2 { match self.evaluate_node_in_context(&args[1], cell) { @@ -539,6 +616,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => 0, + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } } } else { 1 @@ -577,6 +661,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; let start_num = match self.evaluate_node_in_context(&args[1], cell) { CalcResult::Number(v) => { @@ -641,6 +732,13 @@ impl Model { }; } CalcResult::EmptyCell | CalcResult::EmptyArg => 0, + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; let mut result = "".to_string(); let mut count: usize = 0; @@ -983,6 +1081,13 @@ impl Model { } error @ CalcResult::Error { .. } => return error, CalcResult::EmptyArg | CalcResult::Range { .. } => {} + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } } } } @@ -1002,6 +1107,13 @@ impl Model { } } CalcResult::EmptyArg => {} + CalcResult::Array(_) => { + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + } + } }; } let result = values.join(&delimiter); @@ -1125,6 +1237,11 @@ impl Model { } } CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0), + CalcResult::Array(_) => CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Arrays not supported yet".to_string(), + }, } } diff --git a/base/src/functions/util.rs b/base/src/functions/util.rs index 5a6255d..fb04a40 100644 --- a/base/src/functions/util.rs +++ b/base/src/functions/util.rs @@ -393,10 +393,8 @@ pub(crate) fn build_criteria<'a>(value: &'a CalcResult) -> Box { - // TODO: Implicit Intersection - Box::new(move |_x| false) - } + CalcResult::Range { left: _, right: _ } => Box::new(move |_x| false), + CalcResult::Array(_) => Box::new(move |_x| false), CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty), } } diff --git a/base/src/lib.rs b/base/src/lib.rs index ec25fc9..08fc107 100644 --- a/base/src/lib.rs +++ b/base/src/lib.rs @@ -39,6 +39,7 @@ pub mod types; pub mod worksheet; mod actions; +mod arithmetic; mod cast; mod constants; mod functions; diff --git a/base/src/model.rs b/base/src/model.rs index 8364730..7ceb536 100644 --- a/base/src/model.rs +++ b/base/src/model.rs @@ -267,27 +267,10 @@ impl Model { ) -> CalcResult { use Node::*; match node { - OpSumKind { kind, left, right } => { - // In the future once the feature try trait stabilizes we could use the '?' operator for this :) - // See: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=236044e8321a1450988e6ffe5a27dab5 - let l = match self.get_number(left, cell) { - Ok(f) => f, - Err(s) => { - return s; - } - }; - let r = match self.get_number(right, cell) { - Ok(f) => f, - Err(s) => { - return s; - } - }; - let result = match kind { - OpSum::Add => l + r, - OpSum::Minus => l - r, - }; - CalcResult::Number(result) - } + OpSumKind { kind, left, right } => match kind { + OpSum::Add => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 + f2)), + OpSum::Minus => self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 - f2)), + }, NumberKind(value) => CalcResult::Number(*value), StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)), BooleanKind(value) => CalcResult::Boolean(*value), @@ -375,58 +358,26 @@ impl Model { let result = format!("{}{}", l, r); CalcResult::String(result) } - OpProductKind { kind, left, right } => { - let l = match self.get_number(left, cell) { - Ok(f) => f, - Err(s) => { - return s; + OpProductKind { kind, left, right } => match kind { + OpProduct::Times => { + self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1 * f2)) + } + OpProduct::Divide => self.handle_arithmetic(left, right, cell, &|f1, f2| { + if f2 == 0.0 { + Err(Error::DIV) + } else { + Ok(f1 / f2) } - }; - let r = match self.get_number(right, cell) { - Ok(f) => f, - Err(s) => { - return s; - } - }; - let result = match kind { - OpProduct::Times => l * r, - OpProduct::Divide => { - if r == 0.0 { - return CalcResult::new_error( - Error::DIV, - cell, - "Divide by Zero".to_string(), - ); - } - l / r - } - }; - CalcResult::Number(result) - } + }), + }, OpPowerKind { left, right } => { - let l = match self.get_number(left, cell) { - Ok(f) => f, - Err(s) => { - return s; - } - }; - let r = match self.get_number(right, cell) { - Ok(f) => f, - Err(s) => { - return s; - } - }; - // Deal with errors properly - CalcResult::Number(l.powf(r)) + self.handle_arithmetic(left, right, cell, &|f1, f2| Ok(f1.powf(f2))) } FunctionKind { kind, args } => self.evaluate_function(kind, args, cell), InvalidFunctionKind { name, args: _ } => { CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name)) } - ArrayKind(_) => { - // TODO: NOT IMPLEMENTED - CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string()) - } + ArrayKind(s) => CalcResult::Array(s.to_owned()), DefinedNameKind((name, scope, _)) => { if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) { match parsed_defined_name { @@ -704,6 +655,20 @@ impl Model { .get_mut(&column) .expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 }; } + CalcResult::Array(_) => { + *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: "".to_string(), + m: "Arrays not supported yet".to_string(), + ei: Error::NIMPL, + }; + } } } } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index bfe23d2..8e1b4eb 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -51,6 +51,7 @@ mod engineering; mod test_fn_offset; mod test_number_format; +mod test_arrays; mod test_escape_quotes; mod test_extend; mod test_fn_fv; diff --git a/base/src/test/test_arrays.rs b/base/src/test/test_arrays.rs new file mode 100644 index 0000000..35f41ca --- /dev/null +++ b/base/src/test/test_arrays.rs @@ -0,0 +1,13 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn sum_arrays() { + let mut model = new_empty_model(); + model._set("A1", "=SUM({1,2,3}+{3,4,5})"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"18"); +} diff --git a/base/src/test/test_fn_sum.rs b/base/src/test/test_fn_sum.rs index fa680d0..05c47b9 100644 --- a/base/src/test/test_fn_sum.rs +++ b/base/src/test/test_fn_sum.rs @@ -17,3 +17,19 @@ fn test_fn_sum_arguments() { assert_eq!(model._get_text("A3"), *"1"); assert_eq!(model._get_text("A4"), *"4"); } + +#[test] +fn arrays() { + let mut model = new_empty_model(); + model._set("A1", "=SUM({1, 2, 3})"); + model._set("A2", "=SUM({1; 2; 3})"); + model._set("A3", "=SUM({1, 2; 3, 4})"); + model._set("A4", "=SUM({1, 2; 3, 4; 5, 6})"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"6"); + assert_eq!(model._get_text("A2"), *"6"); + assert_eq!(model._get_text("A3"), *"10"); + assert_eq!(model._get_text("A4"), *"21"); +} diff --git a/base/src/test/test_implicit_intersection.rs b/base/src/test/test_implicit_intersection.rs index 43a039b..59687c1 100644 --- a/base/src/test/test_implicit_intersection.rs +++ b/base/src/test/test_implicit_intersection.rs @@ -31,7 +31,7 @@ fn return_of_array_is_n_impl() { model.evaluate(); assert_eq!(model._get_text("C2"), "#N/IMPL!".to_string()); - assert_eq!(model._get_text("D2"), "#N/IMPL!".to_string()); + assert_eq!(model._get_text("D2"), "1.89188842".to_string()); } #[test] diff --git a/webapp/app.ironcalc.com/frontend/package-lock.json b/webapp/app.ironcalc.com/frontend/package-lock.json index 1bd5d66..7347641 100644 --- a/webapp/app.ironcalc.com/frontend/package-lock.json +++ b/webapp/app.ironcalc.com/frontend/package-lock.json @@ -28,6 +28,7 @@ } }, "../../IronCalc": { + "name": "@ironcalc/workbook", "version": "0.3.2", "dependencies": { "@emotion/react": "^11.14.0", diff --git a/webapp/app.ironcalc.com/server/Cargo.lock b/webapp/app.ironcalc.com/server/Cargo.lock index cf8f1cd..0fe13c1 100644 --- a/webapp/app.ironcalc.com/server/Cargo.lock +++ b/webapp/app.ironcalc.com/server/Cargo.lock @@ -925,7 +925,7 @@ dependencies = [ [[package]] name = "ironcalc" -version = "0.2.0" +version = "0.5.0" dependencies = [ "bitcode", "chrono", @@ -940,7 +940,7 @@ dependencies = [ [[package]] name = "ironcalc_base" -version = "0.2.0" +version = "0.5.0" dependencies = [ "bitcode", "chrono", @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "ironcalc_server" -version = "0.1.0" +version = "0.5.0" dependencies = [ "ironcalc", "rand", diff --git a/xlsx/src/import/worksheets.rs b/xlsx/src/import/worksheets.rs index a446660..322c9dc 100644 --- a/xlsx/src/import/worksheets.rs +++ b/xlsx/src/import/worksheets.rs @@ -808,8 +808,9 @@ pub(super) fn load_sheet( // r: reference. A1 style // s: style index // t: cell type - // Unused attributes - // cm (cell metadata), ph (Show Phonetic), vm (value metadata) + // cm: cell metadata (used for dynamic arrays) + // vm: value metadata (used for #SPILL! and #CALC! errors) + // ph: Show Phonetic, unused for cell in row.children() { let cell_ref = get_attribute(&cell, "r")?; let column_letter = get_column_from_ref(cell_ref); @@ -825,6 +826,8 @@ pub(super) fn load_sheet( None }; + let cell_metadata = cell.attribute("cm"); + // type, the default type being "n" for number // If the cell does not have a value is an empty cell let cell_type = match cell.attribute("t") { @@ -934,13 +937,16 @@ pub(super) fn load_sheet( } } } - "array" => { - return Err(XlsxError::NotImplemented("array formulas".to_string())); - } "dataTable" => { return Err(XlsxError::NotImplemented("data table formulas".to_string())); } - "normal" => { + "array" | "normal" => { + let is_dynamic_array = cell_metadata == Some("1"); + if formula_type == "array" && !is_dynamic_array { + // Dynamic formulas in Excel are formulas of type array with the cm=1, those we support. + // On the other hand the old CSE formulas or array formulas are not supported in IronCalc for the time being + return Err(XlsxError::NotImplemented("array formulas".to_string())); + } // Its a cell with a simple formula let formula = fs[0].text().unwrap_or("").to_string(); let context = format!("{}!{}", sheet_name, cell_ref); diff --git a/xlsx/tests/calc_tests/simple_arrays.xlsx b/xlsx/tests/calc_tests/simple_arrays.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e803f3f8398a920b5067516f231ebe0e219a9531 GIT binary patch literal 9494 zcmeHN^;=Z?_8v+a2BZWe1?f)dlpY!cX^>_p=`I05=`NA(4(aYj>24&20f+pK-g~~D z<2m;)xOe}s_cPC)cRinY-?i3ft*tDNfQScp06+l%0F(epxnx~SH~;`02>^HqK!Mkl z0NXj6**P1iyW5*N>9M%k+E8R8!qa5|;9>v&-}PU-0yS}CO7QH!-ecOp!#~`{(&uez+-(P}zn z%?P!gfhPa&TzS|Ku0gLUfFJks~={9y!`%3Ff-HIog76nQHZ zt7P4{1pQ#-KmnGtNDGrk0+>8xLdFP;kKg7irYuJzPIrgzu~eKDJ#s6L_e+w2&JU8a zY%PC~OXd&J+1jHuV)L2)pbK3x%)Cr^M@tvhgCXvP6wCHq`V2&vKt9S`JVo`KPH&3N ze=;my_Nj6-HcE_6w}o?j-=JUB%Hq=-C$>{T3|5TELss-l`)=oz{HGOl2h^RPs8-oTH^Fk=_HA!`a&sc`-$Az{sSVu2wA zC*zrY`!}me#P;+{9V`waFPz{qufj{s2qi$ZHP*r?B8ZxmnxM29KVz|qXc ziIwHY{eQ~*FP7tG&x43u$)OD_Qu54IL4wyjQTK_G=xAX*CYtpbX89Vu&oNPV}&9_w}0V%k=ta%ulW;A7J zODVT3tT4SRmDVVijg)E-qXsA~)S4uei}-3-Emu=~wfh zv^%O9JKG)Eu8v>G;523FD93sna>Z%uoNF~zV3}VP!>Fk=3`j8UTDI-?Nk4CXz}Ug_ zPJv6PAF4x4qdU>6DG&UHBEoKKBS2&g0``_@D%7c0Hd^5C+LO|XbS7*rWL9Ohuh$%v;qO;q8 zgV;)4?|H|L+OE?35u-ewYZXe>5~R8~{*(?Qu5I!15Xn^d<|<}1&5(@m1HRelBHBaV zs+h$AX42tUwtkKk*;C$7@hb;Pi}E7xO787$=MPoS^B6xpW=8B2Vl^1^?YI_^9}~g& zc$oT5-N3p&&GwVli{0GHmJM5a9*Pi(Ad8GXnp4q7y|37oPgONmaf~l3E(tfV@!4qo zi+RQFQs%n7P^ow`H$t^Ln4V~+G_~SO`xrx`kAqR`)cQsrNbh3iuE8T&b=lMe?t;&A z&Tt~Rd5E$Db6pcHrBKgI

>W7Y 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 IQ~mh$e*@c?iU0rr literal 0 HcmV?d00001