# 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
436 lines
15 KiB
Rust
436 lines
15 KiB
Rust
use super::{
|
|
stringify::{stringify_reference, DisplaceData},
|
|
ArrayNode, Node, Reference,
|
|
};
|
|
use crate::{
|
|
constants::{LAST_COLUMN, LAST_ROW},
|
|
expressions::token::OpUnary,
|
|
};
|
|
use crate::{
|
|
expressions::types::{Area, CellReferenceRC},
|
|
number_format::to_excel_precision_str,
|
|
};
|
|
|
|
pub(crate) fn ref_is_in_area(sheet: u32, row: i32, column: i32, area: &Area) -> bool {
|
|
if area.sheet != sheet {
|
|
return false;
|
|
}
|
|
if row < area.row || row > area.row + area.height - 1 {
|
|
return false;
|
|
}
|
|
if column < area.column || column > area.column + area.width - 1 {
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
|
|
pub(crate) struct MoveContext<'a> {
|
|
pub source_sheet_name: &'a str,
|
|
pub row: i32,
|
|
pub column: i32,
|
|
pub area: &'a Area,
|
|
pub target_sheet_name: &'a str,
|
|
pub row_delta: i32,
|
|
pub column_delta: i32,
|
|
}
|
|
|
|
/// This implements Excel's cut && paste
|
|
/// We are moving a formula in (row, column) to (row+row_delta, column + column_delta).
|
|
/// All references that do not point to a cell in area will be left untouched.
|
|
/// All references that point to a cell in area will be displaced
|
|
pub(crate) fn move_formula(node: &Node, move_context: &MoveContext) -> String {
|
|
to_string_moved(node, move_context)
|
|
}
|
|
|
|
fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> String {
|
|
let mut first = true;
|
|
let mut arguments = "".to_string();
|
|
for el in args {
|
|
if !first {
|
|
arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
|
|
} else {
|
|
first = false;
|
|
arguments = to_string_moved(el, move_context);
|
|
}
|
|
}
|
|
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 {
|
|
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
|
NumberKind(number) => to_excel_precision_str(*number),
|
|
StringKind(value) => format!("\"{}\"", value),
|
|
ReferenceKind {
|
|
sheet_name,
|
|
sheet_index,
|
|
absolute_row,
|
|
absolute_column,
|
|
row,
|
|
column,
|
|
} => {
|
|
let reference_row = if *absolute_row {
|
|
*row
|
|
} else {
|
|
row + move_context.row
|
|
};
|
|
let reference_column = if *absolute_column {
|
|
*column
|
|
} else {
|
|
column + move_context.column
|
|
};
|
|
|
|
let new_row;
|
|
let new_column;
|
|
let mut ref_sheet_name = sheet_name;
|
|
let source_sheet_name = &Some(move_context.source_sheet_name.to_string());
|
|
|
|
if ref_is_in_area(
|
|
*sheet_index,
|
|
reference_row,
|
|
reference_column,
|
|
move_context.area,
|
|
) {
|
|
// if the reference is in the area we are moving we want to displace the reference
|
|
new_row = row + move_context.row_delta;
|
|
new_column = column + move_context.column_delta;
|
|
} else {
|
|
// If the reference is not in the area we are moving the reference remains unchanged
|
|
new_row = *row;
|
|
new_column = *column;
|
|
if move_context.target_sheet_name != move_context.source_sheet_name
|
|
&& sheet_name.is_none()
|
|
{
|
|
ref_sheet_name = source_sheet_name;
|
|
}
|
|
};
|
|
let context = CellReferenceRC {
|
|
sheet: move_context.source_sheet_name.to_string(),
|
|
column: move_context.column,
|
|
row: move_context.row,
|
|
};
|
|
stringify_reference(
|
|
Some(&context),
|
|
&DisplaceData::None,
|
|
&Reference {
|
|
sheet_name: ref_sheet_name,
|
|
sheet_index: *sheet_index,
|
|
absolute_row: *absolute_row,
|
|
absolute_column: *absolute_column,
|
|
row: new_row,
|
|
column: new_column,
|
|
},
|
|
false,
|
|
false,
|
|
)
|
|
}
|
|
RangeKind {
|
|
sheet_name,
|
|
sheet_index,
|
|
absolute_row1,
|
|
absolute_column1,
|
|
row1,
|
|
column1,
|
|
absolute_row2,
|
|
absolute_column2,
|
|
row2,
|
|
column2,
|
|
} => {
|
|
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
|
let full_column = *absolute_column1
|
|
&& *absolute_column2
|
|
&& (*column1 == 1)
|
|
&& (*column2 == LAST_COLUMN);
|
|
|
|
let reference_row1 = if *absolute_row1 {
|
|
*row1
|
|
} else {
|
|
row1 + move_context.row
|
|
};
|
|
let reference_column1 = if *absolute_column1 {
|
|
*column1
|
|
} else {
|
|
column1 + move_context.column
|
|
};
|
|
|
|
let reference_row2 = if *absolute_row2 {
|
|
*row2
|
|
} else {
|
|
row2 + move_context.row
|
|
};
|
|
let reference_column2 = if *absolute_column2 {
|
|
*column2
|
|
} else {
|
|
column2 + move_context.column
|
|
};
|
|
|
|
let new_row1;
|
|
let new_column1;
|
|
let new_row2;
|
|
let new_column2;
|
|
let mut ref_sheet_name = sheet_name;
|
|
let source_sheet_name = &Some(move_context.source_sheet_name.to_string());
|
|
if ref_is_in_area(
|
|
*sheet_index,
|
|
reference_row1,
|
|
reference_column1,
|
|
move_context.area,
|
|
) && ref_is_in_area(
|
|
*sheet_index,
|
|
reference_row2,
|
|
reference_column2,
|
|
move_context.area,
|
|
) {
|
|
// if the whole range is inside the area we are moving we want to displace the context
|
|
new_row1 = row1 + move_context.row_delta;
|
|
new_column1 = column1 + move_context.column_delta;
|
|
new_row2 = row2 + move_context.row_delta;
|
|
new_column2 = column2 + move_context.column_delta;
|
|
} else {
|
|
// If the reference is not in the area we are moving the context remains unchanged
|
|
new_row1 = *row1;
|
|
new_column1 = *column1;
|
|
new_row2 = *row2;
|
|
new_column2 = *column2;
|
|
if move_context.target_sheet_name != move_context.source_sheet_name
|
|
&& sheet_name.is_none()
|
|
{
|
|
ref_sheet_name = source_sheet_name;
|
|
}
|
|
};
|
|
let context = CellReferenceRC {
|
|
sheet: move_context.source_sheet_name.to_string(),
|
|
column: move_context.column,
|
|
row: move_context.row,
|
|
};
|
|
let s1 = stringify_reference(
|
|
Some(&context),
|
|
&DisplaceData::None,
|
|
&Reference {
|
|
sheet_name: ref_sheet_name,
|
|
sheet_index: *sheet_index,
|
|
absolute_row: *absolute_row1,
|
|
absolute_column: *absolute_column1,
|
|
row: new_row1,
|
|
column: new_column1,
|
|
},
|
|
full_row,
|
|
full_column,
|
|
);
|
|
let s2 = stringify_reference(
|
|
Some(&context),
|
|
&DisplaceData::None,
|
|
&Reference {
|
|
sheet_name: &None,
|
|
sheet_index: *sheet_index,
|
|
absolute_row: *absolute_row2,
|
|
absolute_column: *absolute_column2,
|
|
row: new_row2,
|
|
column: new_column2,
|
|
},
|
|
full_row,
|
|
full_column,
|
|
);
|
|
format!("{}:{}", s1, s2)
|
|
}
|
|
WrongReferenceKind {
|
|
sheet_name,
|
|
absolute_row,
|
|
absolute_column,
|
|
row,
|
|
column,
|
|
} => {
|
|
// NB: Excel does not displace wrong references but Google Docs does. We follow Excel
|
|
let context = CellReferenceRC {
|
|
sheet: move_context.source_sheet_name.to_string(),
|
|
column: move_context.column,
|
|
row: move_context.row,
|
|
};
|
|
// It's a wrong reference, so there is no valid `sheet_index`.
|
|
// We don't need it, since the `sheet_index` is only used if `displace_data` is not `None`.
|
|
// I should fix it, maybe putting the `sheet_index` inside the `displace_data`
|
|
stringify_reference(
|
|
Some(&context),
|
|
&DisplaceData::None,
|
|
&Reference {
|
|
sheet_name,
|
|
sheet_index: 0, // HACK
|
|
row: *row,
|
|
column: *column,
|
|
absolute_row: *absolute_row,
|
|
absolute_column: *absolute_column,
|
|
},
|
|
false,
|
|
false,
|
|
)
|
|
}
|
|
WrongRangeKind {
|
|
sheet_name,
|
|
absolute_row1,
|
|
absolute_column1,
|
|
row1,
|
|
column1,
|
|
absolute_row2,
|
|
absolute_column2,
|
|
row2,
|
|
column2,
|
|
} => {
|
|
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
|
let full_column = *absolute_column1
|
|
&& *absolute_column2
|
|
&& (*column1 == 1)
|
|
&& (*column2 == LAST_COLUMN);
|
|
|
|
// NB: Excel does not displace wrong references but Google Docs does. We follow Excel
|
|
let context = CellReferenceRC {
|
|
sheet: move_context.source_sheet_name.to_string(),
|
|
column: move_context.column,
|
|
row: move_context.row,
|
|
};
|
|
let s1 = stringify_reference(
|
|
Some(&context),
|
|
&DisplaceData::None,
|
|
&Reference {
|
|
sheet_name,
|
|
sheet_index: 0, // HACK
|
|
row: *row1,
|
|
column: *column1,
|
|
absolute_row: *absolute_row1,
|
|
absolute_column: *absolute_column1,
|
|
},
|
|
full_row,
|
|
full_column,
|
|
);
|
|
let s2 = stringify_reference(
|
|
Some(&context),
|
|
&DisplaceData::None,
|
|
&Reference {
|
|
sheet_name: &None,
|
|
sheet_index: 0, // HACK
|
|
row: *row2,
|
|
column: *column2,
|
|
absolute_row: *absolute_row2,
|
|
absolute_column: *absolute_column2,
|
|
},
|
|
full_row,
|
|
full_column,
|
|
);
|
|
format!("{}:{}", s1, s2)
|
|
}
|
|
OpRangeKind { left, right } => format!(
|
|
"{}:{}",
|
|
to_string_moved(left, move_context),
|
|
to_string_moved(right, move_context),
|
|
),
|
|
OpConcatenateKind { left, right } => format!(
|
|
"{}&{}",
|
|
to_string_moved(left, move_context),
|
|
to_string_moved(right, move_context),
|
|
),
|
|
OpSumKind { kind, left, right } => format!(
|
|
"{}{}{}",
|
|
to_string_moved(left, move_context),
|
|
kind,
|
|
to_string_moved(right, move_context),
|
|
),
|
|
OpProductKind { kind, left, right } => {
|
|
let x = match **left {
|
|
OpSumKind { .. } => format!("({})", to_string_moved(left, move_context)),
|
|
CompareKind { .. } => format!("({})", to_string_moved(left, move_context)),
|
|
_ => to_string_moved(left, move_context),
|
|
};
|
|
let y = match **right {
|
|
OpSumKind { .. } => format!("({})", to_string_moved(right, move_context)),
|
|
CompareKind { .. } => format!("({})", to_string_moved(right, move_context)),
|
|
OpProductKind { .. } => format!("({})", to_string_moved(right, move_context)),
|
|
UnaryKind { .. } => {
|
|
format!("({})", to_string_moved(right, move_context))
|
|
}
|
|
_ => to_string_moved(right, move_context),
|
|
};
|
|
format!("{}{}{}", x, kind, y)
|
|
}
|
|
OpPowerKind { left, right } => format!(
|
|
"{}^{}",
|
|
to_string_moved(left, move_context),
|
|
to_string_moved(right, move_context),
|
|
),
|
|
InvalidFunctionKind { name, args } => move_function(name, args, move_context),
|
|
FunctionKind { kind, args } => {
|
|
let name = &kind.to_string();
|
|
move_function(name, args, move_context)
|
|
}
|
|
ArrayKind(args) => {
|
|
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_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('}');
|
|
}
|
|
|
|
// Enclose the whole matrix in braces
|
|
format!("{{{}}}", matrix_string)
|
|
}
|
|
DefinedNameKind((name, ..)) => name.to_string(),
|
|
TableNameKind(name) => name.to_string(),
|
|
WrongVariableKind(name) => name.to_string(),
|
|
CompareKind { kind, left, right } => format!(
|
|
"{}{}{}",
|
|
to_string_moved(left, move_context),
|
|
kind,
|
|
to_string_moved(right, move_context),
|
|
),
|
|
UnaryKind { kind, right } => match kind {
|
|
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
|
|
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
|
|
},
|
|
ErrorKind(kind) => format!("{}", kind),
|
|
ParseErrorKind {
|
|
formula,
|
|
message: _,
|
|
position: _,
|
|
} => formula.to_string(),
|
|
EmptyArgKind => "".to_string(),
|
|
ImplicitIntersection {
|
|
automatic: _,
|
|
child,
|
|
} => {
|
|
format!("@{}", to_string_moved(child, move_context))
|
|
}
|
|
}
|
|
}
|