Files
IronCalc/base/src/expressions/parser/move_formula.rs
Nicolás Hatcher e5ec75495a 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
2025-03-17 20:04:47 +01:00

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))
}
}
}