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 0000000..e803f3f Binary files /dev/null and b/xlsx/tests/calc_tests/simple_arrays.xlsx differ