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
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
e07fdd2091
commit
e5ec75495a
158
base/src/arithmetic.rs
Normal file
158
base/src/arithmetic.rs
Normal file
@@ -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<f64, Error> {
|
||||
match value {
|
||||
ArrayNode::Number(f) => Ok(*f),
|
||||
ArrayNode::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
|
||||
ArrayNode::String(s) => match s.parse::<f64>() {
|
||||
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<f64, Error>,
|
||||
) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<ArrayNode>>),
|
||||
}
|
||||
|
||||
impl CalcResult {
|
||||
|
||||
@@ -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<Vec<ArrayNode>>),
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn get_number_or_array(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
cell: CellReferenceIndex,
|
||||
) -> Result<NumberOrArray, CalcResult> {
|
||||
match self.evaluate_node_in_context(node, cell) {
|
||||
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
|
||||
CalcResult::String(s) => match s.parse::<f64>() {
|
||||
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(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Node>,
|
||||
},
|
||||
ArrayKind(Vec<Node>),
|
||||
ArrayKind(Vec<Vec<ArrayNode>>),
|
||||
DefinedNameKind(DefinedNameS),
|
||||
TableNameKind(String),
|
||||
WrongVariableKind(String),
|
||||
@@ -454,6 +462,49 @@ impl Parser {
|
||||
self.parse_primary()
|
||||
}
|
||||
|
||||
fn parse_array_row(&mut self) -> Result<Vec<ArrayNode>, 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<Node> = 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,
|
||||
|
||||
@@ -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<Node>, 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<T>`).
|
||||
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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod test_add_implicit_intersection;
|
||||
mod test_arrays;
|
||||
mod test_general;
|
||||
mod test_implicit_intersection;
|
||||
mod test_issue_155;
|
||||
|
||||
92
base/src/expressions/parser/tests/test_arrays.rs
Normal file
92
base/src/expressions/parser/tests/test_arrays.rs
Normal file
@@ -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}");
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
100
base/src/functions/macros.rs
Normal file
100
base/src/functions/macros.rs
Normal file
@@ -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::<f64>() {
|
||||
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,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -15,6 +15,7 @@ mod financial_util;
|
||||
mod information;
|
||||
mod logical;
|
||||
mod lookup_and_reference;
|
||||
mod macros;
|
||||
mod mathematical;
|
||||
mod statistical;
|
||||
mod subtotal;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -393,10 +393,8 @@ pub(crate) fn build_criteria<'a>(value: &'a CalcResult) -> Box<dyn Fn(&CalcResul
|
||||
// An error will match an error (never a string that is an error)
|
||||
Box::new(move |x| result_is_equal_to_error(x, &error.to_string()))
|
||||
}
|
||||
CalcResult::Range { left: _, right: _ } => {
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ pub mod types;
|
||||
pub mod worksheet;
|
||||
|
||||
mod actions;
|
||||
mod arithmetic;
|
||||
mod cast;
|
||||
mod constants;
|
||||
mod functions;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
13
base/src/test/test_arrays.rs
Normal file
13
base/src/test/test_arrays.rs
Normal file
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
}
|
||||
},
|
||||
"../../IronCalc": {
|
||||
"name": "@ironcalc/workbook",
|
||||
"version": "0.3.2",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
|
||||
6
webapp/app.ironcalc.com/server/Cargo.lock
generated
6
webapp/app.ironcalc.com/server/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -808,8 +808,9 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
|
||||
// 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<R: Read + std::io::Seek>(
|
||||
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<R: Read + std::io::Seek>(
|
||||
}
|
||||
}
|
||||
}
|
||||
"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);
|
||||
|
||||
BIN
xlsx/tests/calc_tests/simple_arrays.xlsx
Normal file
BIN
xlsx/tests/calc_tests/simple_arrays.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user