# 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
288 lines
11 KiB
Rust
288 lines
11 KiB
Rust
use crate::{
|
|
calc_result::CalcResult,
|
|
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
|
model::Model,
|
|
};
|
|
|
|
use super::util::compare_values;
|
|
|
|
impl Model {
|
|
pub(crate) fn fn_true(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.is_empty() {
|
|
CalcResult::Boolean(true)
|
|
} else {
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn fn_false(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.is_empty() {
|
|
CalcResult::Boolean(false)
|
|
} else {
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn fn_if(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 2 || args.len() == 3 {
|
|
let cond_result = self.get_boolean(&args[0], cell);
|
|
let cond = match cond_result {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
if cond {
|
|
return self.evaluate_node_in_context(&args[1], cell);
|
|
} else if args.len() == 3 {
|
|
return self.evaluate_node_in_context(&args[2], cell);
|
|
} else {
|
|
return CalcResult::Boolean(false);
|
|
}
|
|
}
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
|
|
pub(crate) fn fn_iferror(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 2 {
|
|
let value = self.evaluate_node_in_context(&args[0], cell);
|
|
match value {
|
|
CalcResult::Error { .. } => {
|
|
return self.evaluate_node_in_context(&args[1], cell);
|
|
}
|
|
_ => return value,
|
|
}
|
|
}
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
|
|
pub(crate) fn fn_ifna(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 2 {
|
|
let value = self.evaluate_node_in_context(&args[0], cell);
|
|
if let CalcResult::Error { error, .. } = &value {
|
|
if error == &Error::NA {
|
|
return self.evaluate_node_in_context(&args[1], cell);
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
|
|
pub(crate) fn fn_not(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 1 {
|
|
match self.get_boolean(&args[0], cell) {
|
|
Ok(f) => return CalcResult::Boolean(!f),
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
}
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
|
|
pub(crate) fn fn_and(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
self.logical_nary(
|
|
args,
|
|
cell,
|
|
|acc, value| acc.unwrap_or(true) && value,
|
|
Some(false),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn fn_or(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
self.logical_nary(
|
|
args,
|
|
cell,
|
|
|acc, value| acc.unwrap_or(false) || value,
|
|
Some(true),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn fn_xor(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
self.logical_nary(args, cell, |acc, value| acc.unwrap_or(false) ^ value, None)
|
|
}
|
|
|
|
/// Base function for AND, OR, XOR. These are all n-ary functions that perform a boolean operation on a series of
|
|
/// boolean values. These boolean values are sourced from `args`. Note that there is not a 1-1 relationship between
|
|
/// arguments and boolean values evaluated (see how Ranges are handled for example).
|
|
///
|
|
/// Each argument in `args` is evaluated and the resulting value is interpreted as a boolean as follows:
|
|
/// - Boolean: The value is used directly.
|
|
/// - Number: 0 is FALSE, all other values are TRUE.
|
|
/// - Range: Each cell in the range is evaluated as if they were individual arguments with some caveats
|
|
/// - Empty arg: FALSE
|
|
/// - Empty cell & String: Ignored, behaves exactly like the argument wasn't passed in at all
|
|
/// - Error: Propagated
|
|
///
|
|
/// If no arguments are provided, or all arguments are ignored, the function returns a #VALUE! error
|
|
///
|
|
/// **`fold_fn`:** The function that combines the running result with the next value boolean value. The running result
|
|
/// starts as `None`.
|
|
///
|
|
/// **`short_circuit_value`:** If the running result reaches `short_circuit_value`, the function returns early.
|
|
fn logical_nary(
|
|
&mut self,
|
|
args: &[Node],
|
|
cell: CellReferenceIndex,
|
|
fold_fn: fn(Option<bool>, bool) -> bool,
|
|
short_circuit_value: Option<bool>,
|
|
) -> CalcResult {
|
|
if args.is_empty() {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
|
|
let mut result = None;
|
|
for arg in args {
|
|
match self.evaluate_node_in_context(arg, cell) {
|
|
CalcResult::Boolean(value) => result = Some(fold_fn(result, value)),
|
|
CalcResult::Number(value) => result = Some(fold_fn(result, value != 0.0)),
|
|
CalcResult::Range { left, right } => {
|
|
if left.sheet != right.sheet {
|
|
return CalcResult::new_error(
|
|
Error::VALUE,
|
|
cell,
|
|
"Ranges are in different sheets".to_string(),
|
|
);
|
|
}
|
|
for row in left.row..(right.row + 1) {
|
|
for column in left.column..(right.column + 1) {
|
|
match self.evaluate_cell(CellReferenceIndex {
|
|
sheet: left.sheet,
|
|
row,
|
|
column,
|
|
}) {
|
|
CalcResult::Boolean(value) => result = Some(fold_fn(result, value)),
|
|
CalcResult::Number(value) => {
|
|
result = Some(fold_fn(result, value != 0.0))
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::EmptyArg => {} // unreachable
|
|
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)
|
|
{
|
|
if current_result == short_circuit_value {
|
|
return CalcResult::Boolean(current_result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::EmptyArg => result = Some(result.unwrap_or(false)),
|
|
// Strings are ignored unless they are "TRUE" or "FALSE" (case insensitive). EXCEPT if the string value
|
|
// comes from a reference, in which case it is always ignored regardless of its value.
|
|
CalcResult::String(..) => {
|
|
if !matches!(arg, Node::ReferenceKind { .. }) {
|
|
if let Ok(f) = self.get_boolean(arg, cell) {
|
|
result = Some(fold_fn(result, f));
|
|
}
|
|
}
|
|
}
|
|
// 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)
|
|
{
|
|
if current_result == short_circuit_value {
|
|
return CalcResult::Boolean(current_result);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(result) = result {
|
|
CalcResult::Boolean(result)
|
|
} else {
|
|
CalcResult::new_error(
|
|
Error::VALUE,
|
|
cell,
|
|
"No logical values in argument list".to_string(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// =SWITCH(expression, case1, value1, [case, value]*, [default])
|
|
pub(crate) fn fn_switch(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let args_count = args.len();
|
|
if args_count < 3 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
// TODO add implicit intersection
|
|
let expr = self.evaluate_node_in_context(&args[0], cell);
|
|
if expr.is_error() {
|
|
return expr;
|
|
}
|
|
|
|
// How many cases we have?
|
|
// 3, 4 args -> 1 case
|
|
let case_count = (args_count - 1) / 2;
|
|
for case_index in 0..case_count {
|
|
let case = self.evaluate_node_in_context(&args[2 * case_index + 1], cell);
|
|
if case.is_error() {
|
|
return case;
|
|
}
|
|
if compare_values(&expr, &case) == 0 {
|
|
return self.evaluate_node_in_context(&args[2 * case_index + 2], cell);
|
|
}
|
|
}
|
|
// None of the cases matched so we return the default
|
|
// If there is an even number of args is the last one otherwise is #N/A
|
|
if args_count % 2 == 0 {
|
|
return self.evaluate_node_in_context(&args[args_count - 1], cell);
|
|
}
|
|
CalcResult::Error {
|
|
error: Error::NA,
|
|
origin: cell,
|
|
message: "Did not find a match".to_string(),
|
|
}
|
|
}
|
|
|
|
/// =IFS(condition1, value, [condition, value]*)
|
|
pub(crate) fn fn_ifs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let args_count = args.len();
|
|
if args_count < 2 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
if args_count % 2 != 0 {
|
|
// Missing value for last condition
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let case_count = args_count / 2;
|
|
for case_index in 0..case_count {
|
|
let value = self.get_boolean(&args[2 * case_index], cell);
|
|
match value {
|
|
Ok(b) => {
|
|
if b {
|
|
return self.evaluate_node_in_context(&args[2 * case_index + 1], cell);
|
|
}
|
|
}
|
|
Err(s) => return s,
|
|
}
|
|
}
|
|
CalcResult::Error {
|
|
error: Error::NA,
|
|
origin: cell,
|
|
message: "Did not find a match".to_string(),
|
|
}
|
|
}
|
|
}
|