UPDATE: Implement the implicit Intersection Operator
The II operator takes a range and returns a single cell that is in the same column or the same row as the present cell. This is needed for backwards compatibility with old Excel models and as a first step towards dynamic arrays. In the past Excel would evaluate `=A1:A10` in cell `C3` as `A3`, but today in results in an array containing all values in the range. To be compatible with old workbooks Excel inserts the II operator on those cases. So this PR performs an static analysis on all formulas inserting on import automatically the II operator where necessary. This we call the _automatic implicit operator_. When exporting to Excel the operator is striped away. You can also manually use the II. For instance `=SUM(@A1:A10)` in cell `C3`. This was not possible before and such a formula would break backwards compatibility with Excel. To Excel that "non automatic" form of the II is exported as `_xlfn.SINGLE()`. Th static analysis has to be done for all arithmetic operations and all functions. This is a bit of a daunting task and it is not done fully in this PR. We also need to implement arrays and dynamic arrays. My believe is that once the core operations have been implemented we can go formula by formula writing proper tests and documentation. After this PR formulas like `=A1:A10` for instance will return `#N/IMPL!` instead of performing the implicit intersection
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
90763048bc
commit
da017b6113
@@ -1,5 +1,6 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::stringify::DisplaceData;
|
||||
use crate::expressions::parser::stringify::{to_string, to_string_displaced, DisplaceData};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use crate::model::Model;
|
||||
|
||||
// NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns
|
||||
@@ -8,16 +9,45 @@ use crate::model::Model;
|
||||
// I feel this is unimportant for now.
|
||||
|
||||
impl Model {
|
||||
fn shift_cell_formula(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
displace_data: &DisplaceData,
|
||||
) -> Result<(), String> {
|
||||
if let Some(f) = self
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(row, column)
|
||||
.and_then(|c| c.get_formula())
|
||||
{
|
||||
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
// FIXME: This is not a very performant way if the formula has changed :S.
|
||||
let formula = to_string(node, &cell_reference);
|
||||
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
|
||||
if formula != formula_displaced {
|
||||
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// This function iterates over all cells in the model and shifts their formulas according to the displacement data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `displace_data` - A reference to `DisplaceData` describing the displacement's direction and magnitude.
|
||||
fn displace_cells(&mut self, displace_data: &DisplaceData) {
|
||||
fn displace_cells(&mut self, displace_data: &DisplaceData) -> Result<(), String> {
|
||||
let cells = self.get_all_cells();
|
||||
for cell in cells {
|
||||
self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data);
|
||||
self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves the column indices for a specific row in a given sheet, sorted in ascending or descending order.
|
||||
@@ -134,7 +164,7 @@ impl Model {
|
||||
column,
|
||||
delta: column_count,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
|
||||
// In the list of columns:
|
||||
// * Keep all the columns to the left
|
||||
@@ -214,7 +244,7 @@ impl Model {
|
||||
column,
|
||||
delta: -column_count,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
|
||||
// deletes all the column styles
|
||||
@@ -338,7 +368,7 @@ impl Model {
|
||||
row,
|
||||
delta: row_count,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -399,7 +429,7 @@ impl Model {
|
||||
row,
|
||||
delta: -row_count,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -420,14 +450,14 @@ impl Model {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
) -> Result<(), &'static str> {
|
||||
) -> Result<(), String> {
|
||||
// Check boundaries
|
||||
let target_column = column + delta;
|
||||
if !(1..=LAST_COLUMN).contains(&target_column) {
|
||||
return Err("Target column out of boundaries");
|
||||
return Err("Target column out of boundaries".to_string());
|
||||
}
|
||||
if !(1..=LAST_COLUMN).contains(&column) {
|
||||
return Err("Initial column out of boundaries");
|
||||
return Err("Initial column out of boundaries".to_string());
|
||||
}
|
||||
|
||||
// TODO: Add the actual displacement of data and styles
|
||||
@@ -439,7 +469,7 @@ impl Model {
|
||||
column,
|
||||
delta,
|
||||
}),
|
||||
);
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, Range},
|
||||
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
||||
implicit_intersection::implicit_intersection,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
@@ -39,21 +38,13 @@ impl Model {
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => {
|
||||
let result = self.evaluate_cell(cell_reference);
|
||||
self.cast_to_number(result, cell_reference)
|
||||
}
|
||||
None => Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
CalcResult::Range { .. } => Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Invalid reference (number)".to_string(),
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_number_no_bools(
|
||||
&mut self,
|
||||
@@ -99,21 +90,13 @@ impl Model {
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => {
|
||||
let result = self.evaluate_cell(cell_reference);
|
||||
self.cast_to_string(result, cell_reference)
|
||||
}
|
||||
None => Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
CalcResult::Range { .. } => Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Invalid reference (string)".to_string(),
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_boolean(
|
||||
&mut self,
|
||||
@@ -151,21 +134,13 @@ impl Model {
|
||||
CalcResult::Boolean(b) => Ok(b),
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => {
|
||||
let result = self.evaluate_cell(cell_reference);
|
||||
self.cast_to_bool(result, cell_reference)
|
||||
}
|
||||
None => Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
CalcResult::Range { .. } => Err(CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Invalid reference (bool)".to_string(),
|
||||
message: "Arrays not supported yet".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tries to return a reference. That is either a reference or a formula that evaluates to a range/reference
|
||||
pub(crate) fn get_reference(
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
use crate::{
|
||||
expressions::{
|
||||
parser::{
|
||||
move_formula::ref_is_in_area,
|
||||
stringify::{to_string, to_string_displaced, DisplaceData},
|
||||
walk::forward_references,
|
||||
},
|
||||
types::{Area, CellReferenceIndex, CellReferenceRC},
|
||||
},
|
||||
model::Model,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
pub enum CellValue {
|
||||
Value(String),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SetCellValue {
|
||||
cell: CellReferenceIndex,
|
||||
new_value: CellValue,
|
||||
old_value: CellValue,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
#[allow(clippy::expect_used)]
|
||||
pub(crate) fn shift_cell_formula(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
displace_data: &DisplaceData,
|
||||
) {
|
||||
if let Some(f) = self
|
||||
.workbook
|
||||
.worksheet(sheet)
|
||||
.expect("Worksheet must exist")
|
||||
.cell(row, column)
|
||||
.expect("Cell must exist")
|
||||
.get_formula()
|
||||
{
|
||||
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
// FIXME: This is not a very performant way if the formula has changed :S.
|
||||
let formula = to_string(node, &cell_reference);
|
||||
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
|
||||
if formula != formula_displaced {
|
||||
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))
|
||||
.expect("Failed to shift cell formula");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
pub fn forward_references(
|
||||
&mut self,
|
||||
source_area: &Area,
|
||||
target: &CellReferenceIndex,
|
||||
) -> Result<Vec<SetCellValue>, String> {
|
||||
let mut diff_list: Vec<SetCellValue> = Vec::new();
|
||||
let target_area = &Area {
|
||||
sheet: target.sheet,
|
||||
row: target.row,
|
||||
column: target.column,
|
||||
width: source_area.width,
|
||||
height: source_area.height,
|
||||
};
|
||||
// Walk over every formula
|
||||
let cells = self.get_all_cells();
|
||||
for cell in cells {
|
||||
if let Some(f) = self
|
||||
.workbook
|
||||
.worksheet(cell.index)
|
||||
.expect("Worksheet must exist")
|
||||
.cell(cell.row, cell.column)
|
||||
.expect("Cell must exist")
|
||||
.get_formula()
|
||||
{
|
||||
let sheet = cell.index;
|
||||
let row = cell.row;
|
||||
let column = cell.column;
|
||||
|
||||
// If cell is in the source or target area, skip
|
||||
if ref_is_in_area(sheet, row, column, source_area)
|
||||
|| ref_is_in_area(sheet, row, column, target_area)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the formula
|
||||
// Get a copy of the AST
|
||||
let node = &mut self.parsed_formulas[sheet as usize][f as usize].clone();
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
column: cell.column,
|
||||
row: cell.row,
|
||||
};
|
||||
let context = CellReferenceIndex { sheet, column, row };
|
||||
let formula = to_string(node, &cell_reference);
|
||||
let target_sheet_name = &self.workbook.worksheets[target.sheet as usize].name;
|
||||
forward_references(
|
||||
node,
|
||||
&context,
|
||||
source_area,
|
||||
target.sheet,
|
||||
target_sheet_name,
|
||||
target.row,
|
||||
target.column,
|
||||
);
|
||||
|
||||
// If the string representation of the formula has changed update the cell
|
||||
let updated_formula = to_string(node, &cell_reference);
|
||||
if formula != updated_formula {
|
||||
self.update_cell_with_formula(
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
format!("={updated_formula}"),
|
||||
)?;
|
||||
// Update the diff list
|
||||
diff_list.push(SetCellValue {
|
||||
cell: CellReferenceIndex { sheet, column, row },
|
||||
new_value: CellValue::Value(format!("={}", updated_formula)),
|
||||
old_value: CellValue::Value(format!("={}", formula)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(diff_list)
|
||||
}
|
||||
}
|
||||
@@ -187,6 +187,7 @@ impl Lexer {
|
||||
']' => TokenType::RightBracket,
|
||||
':' => TokenType::Colon,
|
||||
';' => TokenType::Semicolon,
|
||||
'@' => TokenType::At,
|
||||
',' => {
|
||||
if self.locale.numbers.symbols.decimal == "," {
|
||||
match self.consume_number(',') {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod test_common;
|
||||
mod test_implicit_intersection;
|
||||
mod test_language;
|
||||
mod test_locale;
|
||||
mod test_ranges;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::TokenType::*,
|
||||
};
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
fn new_lexer(formula: &str) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language("en").unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sum_implicit_intersection() {
|
||||
let mut lx = new_lexer("sum(@A1:A3)");
|
||||
assert_eq!(lx.next_token(), Ident("sum".to_string()));
|
||||
assert_eq!(lx.next_token(), LeftParenthesis);
|
||||
assert_eq!(lx.next_token(), At);
|
||||
assert!(matches!(lx.next_token(), Range { .. }));
|
||||
assert_eq!(lx.next_token(), RightParenthesis);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
# GRAMAR
|
||||
# GRAMMAR
|
||||
|
||||
<pre class="rust">
|
||||
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
@@ -12,7 +12,8 @@ term => factor (opFactor factor)*
|
||||
factor => prod (opProd prod)*
|
||||
prod => power ('^' power)*
|
||||
power => (unaryOp)* range '%'*
|
||||
range => primary (':' primary)?
|
||||
range => implicit (':' primary)?
|
||||
implicit=> '@' primary | primary
|
||||
primary => '(' expr ')'
|
||||
=> number
|
||||
=> function '(' f_args ')'
|
||||
@@ -45,8 +46,8 @@ use super::utils::number_to_column;
|
||||
use token::OpCompare;
|
||||
|
||||
pub mod move_formula;
|
||||
pub mod static_analysis;
|
||||
pub mod stringify;
|
||||
pub mod walk;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -81,6 +82,9 @@ fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option<i3
|
||||
None
|
||||
}
|
||||
|
||||
// DefinedNameS is a tuple with the name of the defined name, the index of the sheet and the formula
|
||||
pub type DefinedNameS = (String, Option<u32>, String);
|
||||
|
||||
pub(crate) struct Reference<'a> {
|
||||
sheet_name: &'a Option<String>,
|
||||
sheet_index: u32,
|
||||
@@ -164,9 +168,13 @@ pub enum Node {
|
||||
args: Vec<Node>,
|
||||
},
|
||||
ArrayKind(Vec<Node>),
|
||||
DefinedNameKind((String, Option<u32>)),
|
||||
DefinedNameKind(DefinedNameS),
|
||||
TableNameKind(String),
|
||||
WrongVariableKind(String),
|
||||
ImplicitIntersection {
|
||||
automatic: bool,
|
||||
child: Box<Node>,
|
||||
},
|
||||
CompareKind {
|
||||
kind: OpCompare,
|
||||
left: Box<Node>,
|
||||
@@ -189,7 +197,7 @@ pub enum Node {
|
||||
pub struct Parser {
|
||||
lexer: lexer::Lexer,
|
||||
worksheets: Vec<String>,
|
||||
defined_names: Vec<(String, Option<u32>)>,
|
||||
defined_names: Vec<DefinedNameS>,
|
||||
context: CellReferenceRC,
|
||||
tables: HashMap<String, Table>,
|
||||
}
|
||||
@@ -197,7 +205,7 @@ pub struct Parser {
|
||||
impl Parser {
|
||||
pub fn new(
|
||||
worksheets: Vec<String>,
|
||||
defined_names: Vec<(String, Option<u32>)>,
|
||||
defined_names: Vec<DefinedNameS>,
|
||||
tables: HashMap<String, Table>,
|
||||
) -> Parser {
|
||||
let lexer = lexer::Lexer::new(
|
||||
@@ -228,7 +236,7 @@ impl Parser {
|
||||
pub fn set_worksheets_and_names(
|
||||
&mut self,
|
||||
worksheets: Vec<String>,
|
||||
defined_names: Vec<(String, Option<u32>)>,
|
||||
defined_names: Vec<DefinedNameS>,
|
||||
) {
|
||||
self.worksheets = worksheets;
|
||||
self.defined_names = defined_names;
|
||||
@@ -252,17 +260,17 @@ impl Parser {
|
||||
|
||||
// Returns:
|
||||
// * None: If there is no defined name by that name
|
||||
// * Some(Some(index)): If there is a defined name local to that sheet
|
||||
// * Some((Some(index), formula)): If there is a defined name local to that sheet
|
||||
// * Some(None): If there is a global defined name
|
||||
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<Option<u32>> {
|
||||
for (df_name, df_scope) in &self.defined_names {
|
||||
fn get_defined_name(&self, name: &str, sheet: u32) -> Option<(Option<u32>, String)> {
|
||||
for (df_name, df_scope, df_formula) in &self.defined_names {
|
||||
if name.to_lowercase() == df_name.to_lowercase() && df_scope == &Some(sheet) {
|
||||
return Some(*df_scope);
|
||||
return Some((*df_scope, df_formula.to_owned()));
|
||||
}
|
||||
}
|
||||
for (df_name, df_scope) in &self.defined_names {
|
||||
for (df_name, df_scope, df_formula) in &self.defined_names {
|
||||
if name.to_lowercase() == df_name.to_lowercase() && df_scope.is_none() {
|
||||
return Some(None);
|
||||
return Some((None, df_formula.to_owned()));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -411,7 +419,7 @@ impl Parser {
|
||||
}
|
||||
|
||||
fn parse_range(&mut self) -> Node {
|
||||
let t = self.parse_primary();
|
||||
let t = self.parse_implicit();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
@@ -430,6 +438,22 @@ impl Parser {
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_implicit(&mut self) -> Node {
|
||||
let next_token = self.lexer.peek_token();
|
||||
if next_token == TokenType::At {
|
||||
self.lexer.advance_token();
|
||||
let t = self.parse_primary();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
return Node::ImplicitIntersection {
|
||||
automatic: false,
|
||||
child: Box::new(t),
|
||||
};
|
||||
}
|
||||
self.parse_primary()
|
||||
}
|
||||
|
||||
fn parse_primary(&mut self) -> Node {
|
||||
let next_token = self.lexer.next_token();
|
||||
match next_token {
|
||||
@@ -604,6 +628,20 @@ impl Parser {
|
||||
args,
|
||||
};
|
||||
}
|
||||
if &name == "_xlfn.SINGLE" {
|
||||
if args.len() != 1 {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: "Implicit Intersection requires just one argument"
|
||||
.to_string(),
|
||||
};
|
||||
}
|
||||
return Node::ImplicitIntersection {
|
||||
automatic: false,
|
||||
child: Box::new(args[0].clone()),
|
||||
};
|
||||
}
|
||||
return Node::InvalidFunctionKind { name, args };
|
||||
}
|
||||
let context = &self.context;
|
||||
@@ -620,8 +658,8 @@ impl Parser {
|
||||
};
|
||||
|
||||
// Could be a defined name or a table
|
||||
if let Some(scope) = self.get_defined_name(&name, context_sheet_index) {
|
||||
return Node::DefinedNameKind((name, scope));
|
||||
if let Some((scope, formula)) = self.get_defined_name(&name, context_sheet_index) {
|
||||
return Node::DefinedNameKind((name, scope, formula));
|
||||
}
|
||||
let name_lower = name.to_lowercase();
|
||||
for table_name in self.tables.keys() {
|
||||
@@ -706,6 +744,14 @@ impl Parser {
|
||||
message: "Unexpected token: 'POWER'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::At => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: '@'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::RightParenthesis
|
||||
| TokenType::RightBracket
|
||||
| TokenType::Colon
|
||||
|
||||
@@ -375,7 +375,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
}
|
||||
format!("{{{}}}", arguments)
|
||||
}
|
||||
DefinedNameKind((name, _)) => name.to_string(),
|
||||
DefinedNameKind((name, ..)) => name.to_string(),
|
||||
TableNameKind(name) => name.to_string(),
|
||||
WrongVariableKind(name) => name.to_string(),
|
||||
CompareKind { kind, left, right } => format!(
|
||||
@@ -395,5 +395,11 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
position: _,
|
||||
} => formula.to_string(),
|
||||
EmptyArgKind => "".to_string(),
|
||||
ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => {
|
||||
format!("@{}", to_string_moved(child, move_context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
984
base/src/expressions/parser/static_analysis.rs
Normal file
984
base/src/expressions/parser/static_analysis.rs
Normal file
@@ -0,0 +1,984 @@
|
||||
use crate::functions::Function;
|
||||
|
||||
use super::Node;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r":[A-Z]*[0-9]*$").expect("Regex is known to be valid"));
|
||||
|
||||
fn is_range_reference(s: &str) -> bool {
|
||||
RE.is_match(s)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
# NOTES on the Implicit Intersection operator: @
|
||||
|
||||
Sometimes we obtain a range where we expected a single argument. This can happen:
|
||||
|
||||
* As an argument of a function, eg: `SIN(A1:A5)`
|
||||
* As the result of a computation of a formula `=A1:A5`
|
||||
|
||||
In previous versions of the Friendly Giant the spreadsheet engine would perform an operation called _implicit intersection_
|
||||
that tries to find a single cell within the range. It works by picking a cell in the range that is the same row or the same column
|
||||
as the cell. If there is just one we return that otherwise we return the `#REF!` error.
|
||||
|
||||
Examples:
|
||||
|
||||
* Siting on `C3` the formula `=D1:D5` will return `D3`
|
||||
* Sitting on `C3` the formula `=D:D` will return `D3`
|
||||
* Sitting on `C3` the formula `=A1:A7` will return `A3`
|
||||
* Sitting on `C3` the formula `=A5:A8` will return `#REF!`
|
||||
* Sitting on `C3` the formula `D1:G7` will return `#REF!`
|
||||
|
||||
Today's version of the engine will result in a dynamic array spilling the result through several cells.
|
||||
To force the old behaviour we can use the _implicit intersection operator_: @
|
||||
|
||||
* `=@A1:A7` or `=SIN(@A1:A7)
|
||||
|
||||
When parsing formulas that come form old workbooks this is done automatically.
|
||||
We call this version of the II operator the _automatic_ II operator.
|
||||
|
||||
We can also insert the II operator in places where before was impossible:
|
||||
|
||||
* `=SUM(@A1:A7)`
|
||||
|
||||
This formulas will not be compatible with old versions of the engine. The FG will stringify this as `=SUM(_xlfn.SIMPLE(A1:A7))`.
|
||||
*/
|
||||
|
||||
/// Transverses the formula tree adding the implicit intersection operator in all arguments of functions that
|
||||
/// expect a scalar but get a range.
|
||||
/// * A:A => @A:A
|
||||
/// * SIN(A1:D1) => SIN(@A1:D1)
|
||||
///
|
||||
/// Assumes formula return a scalar
|
||||
pub fn add_implicit_intersection(node: &mut Node, add: bool) {
|
||||
match node {
|
||||
Node::BooleanKind(_)
|
||||
| Node::NumberKind(_)
|
||||
| Node::StringKind(_)
|
||||
| Node::ErrorKind(_)
|
||||
| Node::EmptyArgKind
|
||||
| Node::ParseErrorKind { .. }
|
||||
| Node::WrongReferenceKind { .. }
|
||||
| Node::WrongRangeKind { .. }
|
||||
| Node::InvalidFunctionKind { .. }
|
||||
| Node::ArrayKind(_)
|
||||
| Node::ReferenceKind { .. } => {}
|
||||
Node::ImplicitIntersection { child, .. } => {
|
||||
// We need to check wether the II can be automatic or not
|
||||
let mut new_node = child.as_ref().clone();
|
||||
add_implicit_intersection(&mut new_node, add);
|
||||
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
|
||||
*node = new_node
|
||||
}
|
||||
}
|
||||
Node::RangeKind {
|
||||
row1,
|
||||
column1,
|
||||
row2,
|
||||
column2,
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
} => {
|
||||
if add {
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(Node::RangeKind {
|
||||
sheet_name: sheet_name.clone(),
|
||||
sheet_index: *sheet_index,
|
||||
absolute_row1: *absolute_row1,
|
||||
absolute_column1: *absolute_column1,
|
||||
row1: *row1,
|
||||
column1: *column1,
|
||||
absolute_row2: *absolute_row2,
|
||||
absolute_column2: *absolute_column2,
|
||||
row2: *row2,
|
||||
column2: *column2,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
Node::OpRangeKind { left, right } => {
|
||||
if add {
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(Node::OpRangeKind {
|
||||
left: left.clone(),
|
||||
right: right.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// operations
|
||||
Node::UnaryKind { right, .. } => add_implicit_intersection(right, add),
|
||||
Node::OpConcatenateKind { left, right }
|
||||
| Node::OpSumKind { left, right, .. }
|
||||
| Node::OpProductKind { left, right, .. }
|
||||
| Node::OpPowerKind { left, right, .. }
|
||||
| Node::CompareKind { left, right, .. } => {
|
||||
add_implicit_intersection(left, add);
|
||||
add_implicit_intersection(right, add);
|
||||
}
|
||||
|
||||
Node::DefinedNameKind(v) => {
|
||||
if add {
|
||||
// Not all defined names deserve the II operator
|
||||
// For instance =Sheet1!A1 doesn't need to be intersected
|
||||
if is_range_reference(&v.2) {
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(Node::DefinedNameKind(v.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::WrongVariableKind(v) => {
|
||||
if add {
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(Node::WrongVariableKind(v.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::TableNameKind(_) => {
|
||||
// noop for now
|
||||
}
|
||||
Node::FunctionKind { kind, args } => {
|
||||
let arg_count = args.len();
|
||||
let signature = get_function_args_signature(kind, arg_count);
|
||||
for index in 0..arg_count {
|
||||
if matches!(signature[index], Signature::Scalar)
|
||||
&& matches!(
|
||||
run_static_analysis_on_node(&args[index]),
|
||||
StaticResult::Range(_, _) | StaticResult::Unknown
|
||||
)
|
||||
{
|
||||
add_implicit_intersection(&mut args[index], true);
|
||||
} else {
|
||||
add_implicit_intersection(&mut args[index], false);
|
||||
}
|
||||
}
|
||||
if add
|
||||
&& matches!(
|
||||
run_static_analysis_on_node(node),
|
||||
StaticResult::Range(_, _) | StaticResult::Unknown
|
||||
)
|
||||
{
|
||||
*node = Node::ImplicitIntersection {
|
||||
automatic: true,
|
||||
child: Box::new(node.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) enum StaticResult {
|
||||
Scalar,
|
||||
Array(i32, i32),
|
||||
Range(i32, i32),
|
||||
Unknown,
|
||||
// TODO: What if one of the dimensions is known?
|
||||
// what if the dimensions are unknown but bounded?
|
||||
}
|
||||
|
||||
fn static_analysis_op_nodes(left: &Node, right: &Node) -> StaticResult {
|
||||
let lhs = run_static_analysis_on_node(left);
|
||||
let rhs = run_static_analysis_on_node(right);
|
||||
match (lhs, rhs) {
|
||||
(StaticResult::Scalar, StaticResult::Scalar) => StaticResult::Scalar,
|
||||
(StaticResult::Scalar, StaticResult::Array(a, b) | StaticResult::Range(a, b)) => {
|
||||
StaticResult::Array(a, b)
|
||||
}
|
||||
|
||||
(StaticResult::Array(a, b) | StaticResult::Range(a, b), StaticResult::Scalar) => {
|
||||
StaticResult::Array(a, b)
|
||||
}
|
||||
(
|
||||
StaticResult::Array(a1, b1) | StaticResult::Range(a1, b1),
|
||||
StaticResult::Array(a2, b2) | StaticResult::Range(a2, b2),
|
||||
) => StaticResult::Array(a1.max(a2), b1.max(b2)),
|
||||
|
||||
(_, StaticResult::Unknown) => StaticResult::Unknown,
|
||||
(StaticResult::Unknown, _) => StaticResult::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
// Returns:
|
||||
// * Scalar if we can proof the result of the evaluation is a scalar
|
||||
// * Array(a, b) if we know it will be an a x b array.
|
||||
// * Range(a, b) if we know it will be a a x b range.
|
||||
// * Unknown if we cannot guaranty either
|
||||
fn run_static_analysis_on_node(node: &Node) -> StaticResult {
|
||||
match node {
|
||||
Node::BooleanKind(_)
|
||||
| Node::NumberKind(_)
|
||||
| Node::StringKind(_)
|
||||
| Node::ErrorKind(_)
|
||||
| Node::EmptyArgKind => StaticResult::Scalar,
|
||||
Node::UnaryKind { right, .. } => run_static_analysis_on_node(right),
|
||||
Node::ParseErrorKind { .. } => {
|
||||
// StaticResult::Unknown is also valid
|
||||
StaticResult::Scalar
|
||||
}
|
||||
Node::WrongReferenceKind { .. } => {
|
||||
// StaticResult::Unknown is also valid
|
||||
StaticResult::Scalar
|
||||
}
|
||||
Node::WrongRangeKind { .. } => {
|
||||
// StaticResult::Unknown or Array is also valid
|
||||
StaticResult::Scalar
|
||||
}
|
||||
Node::InvalidFunctionKind { .. } => {
|
||||
// StaticResult::Unknown is also valid
|
||||
StaticResult::Scalar
|
||||
}
|
||||
Node::ArrayKind(array) => {
|
||||
let n = array.len() as i32;
|
||||
// FIXME: This is a placeholder until we implement arrays
|
||||
StaticResult::Array(n, 1)
|
||||
}
|
||||
Node::RangeKind {
|
||||
row1,
|
||||
column1,
|
||||
row2,
|
||||
column2,
|
||||
..
|
||||
} => StaticResult::Range(row2 - row1, column2 - column1),
|
||||
Node::OpRangeKind { .. } => {
|
||||
// TODO: We could do a bit better here
|
||||
StaticResult::Unknown
|
||||
}
|
||||
Node::ReferenceKind { .. } => StaticResult::Scalar,
|
||||
|
||||
// binary operations
|
||||
Node::OpConcatenateKind { left, right } => static_analysis_op_nodes(left, right),
|
||||
Node::OpSumKind { left, right, .. } => static_analysis_op_nodes(left, right),
|
||||
Node::OpProductKind { left, right, .. } => static_analysis_op_nodes(left, right),
|
||||
Node::OpPowerKind { left, right, .. } => static_analysis_op_nodes(left, right),
|
||||
Node::CompareKind { left, right, .. } => static_analysis_op_nodes(left, right),
|
||||
|
||||
// defined names
|
||||
Node::DefinedNameKind(_) => StaticResult::Unknown,
|
||||
Node::WrongVariableKind(_) => StaticResult::Unknown,
|
||||
Node::TableNameKind(_) => StaticResult::Unknown,
|
||||
Node::FunctionKind { kind, args } => static_analysis_on_function(kind, args),
|
||||
Node::ImplicitIntersection { .. } => StaticResult::Scalar,
|
||||
}
|
||||
}
|
||||
|
||||
// If all the arguments are scalars the function will return a scalar
|
||||
// If any of the arguments is a range or an array it will return an array
|
||||
fn scalar_arguments(args: &[Node]) -> StaticResult {
|
||||
let mut n = 0;
|
||||
let mut m = 0;
|
||||
for arg in args {
|
||||
match run_static_analysis_on_node(arg) {
|
||||
StaticResult::Scalar => {
|
||||
// noop
|
||||
}
|
||||
StaticResult::Array(a, b) | StaticResult::Range(a, b) => {
|
||||
n = n.max(a);
|
||||
m = m.max(b);
|
||||
}
|
||||
StaticResult::Unknown => return StaticResult::Unknown,
|
||||
}
|
||||
}
|
||||
if n == 0 && m == 0 {
|
||||
return StaticResult::Scalar;
|
||||
}
|
||||
StaticResult::Array(n, m)
|
||||
}
|
||||
|
||||
// We only care if the function can return a range or not
|
||||
fn not_implemented(_args: &[Node]) -> StaticResult {
|
||||
StaticResult::Scalar
|
||||
}
|
||||
|
||||
fn static_analysis_offset(args: &[Node]) -> StaticResult {
|
||||
// If first argument is a single cell reference and there are no4th and 5th argument,
|
||||
// or they are 1, then it is a scalar
|
||||
let arg_count = args.len();
|
||||
if arg_count < 3 {
|
||||
// Actually an error
|
||||
return StaticResult::Scalar;
|
||||
}
|
||||
if !matches!(args[0], Node::ReferenceKind { .. }) {
|
||||
return StaticResult::Unknown;
|
||||
}
|
||||
if arg_count == 3 {
|
||||
return StaticResult::Scalar;
|
||||
}
|
||||
match args[3] {
|
||||
Node::NumberKind(f) => {
|
||||
if f != 1.0 {
|
||||
return StaticResult::Unknown;
|
||||
}
|
||||
}
|
||||
_ => return StaticResult::Unknown,
|
||||
};
|
||||
if arg_count == 4 {
|
||||
return StaticResult::Scalar;
|
||||
}
|
||||
match args[4] {
|
||||
Node::NumberKind(f) => {
|
||||
if f != 1.0 {
|
||||
return StaticResult::Unknown;
|
||||
}
|
||||
}
|
||||
_ => return StaticResult::Unknown,
|
||||
};
|
||||
StaticResult::Unknown
|
||||
}
|
||||
|
||||
// fn static_analysis_choose(_args: &[Node]) -> StaticResult {
|
||||
// // We will always insert the @ in CHOOSE, but technically it is only needed if one of the elements is a range
|
||||
// StaticResult::Unknown
|
||||
// }
|
||||
|
||||
fn static_analysis_indirect(_args: &[Node]) -> StaticResult {
|
||||
// We will always insert the @, but we don't need to do that in every scenario`
|
||||
StaticResult::Unknown
|
||||
}
|
||||
|
||||
fn static_analysis_index(_args: &[Node]) -> StaticResult {
|
||||
// INDEX has two forms, but they are indistinguishable at parse time.
|
||||
StaticResult::Unknown
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Signature {
|
||||
Scalar,
|
||||
Vector,
|
||||
Error,
|
||||
}
|
||||
|
||||
fn args_signature_no_args(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 0 {
|
||||
vec![]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_scalars(
|
||||
arg_count: usize,
|
||||
required_count: usize,
|
||||
optional_count: usize,
|
||||
) -> Vec<Signature> {
|
||||
if arg_count >= required_count && arg_count <= required_count + optional_count {
|
||||
vec![Signature::Scalar; arg_count]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_one_vector(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 1 {
|
||||
vec![Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_sumif(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector, Signature::Scalar]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Scalar, Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
// 1 or none scalars
|
||||
fn args_signature_sheet(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 0 {
|
||||
vec![]
|
||||
} else if arg_count == 1 {
|
||||
vec![Signature::Scalar]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_hlookup(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
|
||||
} else if arg_count == 4 {
|
||||
vec![
|
||||
Signature::Vector,
|
||||
Signature::Vector,
|
||||
Signature::Scalar,
|
||||
Signature::Vector,
|
||||
]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_index(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector, Signature::Scalar]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
|
||||
} else if arg_count == 4 {
|
||||
vec![
|
||||
Signature::Vector,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_lookup(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector, Signature::Vector]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Vector, Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_match(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector, Signature::Vector]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_offset(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
|
||||
} else if arg_count == 4 {
|
||||
vec![
|
||||
Signature::Vector,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
]
|
||||
} else if arg_count == 5 {
|
||||
vec![
|
||||
Signature::Vector,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
Signature::Scalar,
|
||||
]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_row(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 0 {
|
||||
vec![]
|
||||
} else if arg_count == 1 {
|
||||
vec![Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_xlookup(arg_count: usize) -> Vec<Signature> {
|
||||
if !(3..=6).contains(&arg_count) {
|
||||
return vec![Signature::Error; arg_count];
|
||||
}
|
||||
let mut result = vec![Signature::Scalar; arg_count];
|
||||
result[0] = Signature::Vector;
|
||||
result[1] = Signature::Vector;
|
||||
result[2] = Signature::Vector;
|
||||
result
|
||||
}
|
||||
|
||||
fn args_signature_textafter(arg_count: usize) -> Vec<Signature> {
|
||||
if !(2..=6).contains(&arg_count) {
|
||||
vec![Signature::Scalar; arg_count]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_textjoin(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count >= 3 {
|
||||
let mut result = vec![Signature::Vector; arg_count];
|
||||
result[0] = Signature::Scalar;
|
||||
result[1] = Signature::Scalar;
|
||||
result
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_npv(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count < 2 {
|
||||
return vec![Signature::Error; arg_count];
|
||||
}
|
||||
let mut result = vec![Signature::Vector; arg_count];
|
||||
result[0] = Signature::Scalar;
|
||||
result
|
||||
}
|
||||
|
||||
fn args_signature_irr(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count > 2 {
|
||||
vec![Signature::Error; arg_count]
|
||||
} else if arg_count == 1 {
|
||||
vec![Signature::Vector]
|
||||
} else {
|
||||
vec![Signature::Vector, Signature::Scalar]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_xirr(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count == 2 {
|
||||
vec![Signature::Vector; arg_count]
|
||||
} else if arg_count == 3 {
|
||||
vec![Signature::Vector, Signature::Vector, Signature::Scalar]
|
||||
} else {
|
||||
vec![Signature::Error; arg_count]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_mirr(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count != 3 {
|
||||
vec![Signature::Error; arg_count]
|
||||
} else {
|
||||
vec![Signature::Vector, Signature::Scalar, Signature::Scalar]
|
||||
}
|
||||
}
|
||||
|
||||
fn args_signature_xnpv(arg_count: usize) -> Vec<Signature> {
|
||||
if arg_count != 3 {
|
||||
vec![Signature::Error; arg_count]
|
||||
} else {
|
||||
vec![Signature::Scalar, Signature::Vector, Signature::Vector]
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This is terrible duplications of efforts. We use the signature in at least three different places:
|
||||
// 1. When computing the function
|
||||
// 2. Checking the arguments to see if we need to insert the implicit intersection operator
|
||||
// 3. Understanding the return value
|
||||
//
|
||||
// The signature of the functions should be defined only once
|
||||
|
||||
// Given a function and a number of arguments this returns the arguments at each position
|
||||
// are expected to be scalars or vectors (array/ranges).
|
||||
// Sets signature::Error to all arguments if the number of arguments is incorrect.
|
||||
fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signature> {
|
||||
match kind {
|
||||
Function::And => vec![Signature::Vector; arg_count],
|
||||
Function::False => args_signature_no_args(arg_count),
|
||||
Function::If => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Iferror => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Ifna => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Ifs => vec![Signature::Scalar; arg_count],
|
||||
Function::Not => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Or => vec![Signature::Vector; arg_count],
|
||||
Function::Switch => vec![Signature::Scalar; arg_count],
|
||||
Function::True => args_signature_no_args(arg_count),
|
||||
Function::Xor => vec![Signature::Vector; arg_count],
|
||||
Function::Abs => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Acos => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Acosh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Asin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Asinh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Atan => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Atan2 => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Atanh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Choose => vec![Signature::Scalar; arg_count],
|
||||
Function::Column => args_signature_row(arg_count),
|
||||
Function::Columns => args_signature_one_vector(arg_count),
|
||||
Function::Cos => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Cosh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Max => vec![Signature::Vector; arg_count],
|
||||
Function::Min => vec![Signature::Vector; arg_count],
|
||||
Function::Pi => args_signature_no_args(arg_count),
|
||||
Function::Power => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Product => vec![Signature::Vector; arg_count],
|
||||
Function::Round => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Rounddown => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Roundup => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Sin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Sinh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Sqrt => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Sqrtpi => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Sum => vec![Signature::Vector; arg_count],
|
||||
Function::Sumif => args_signature_sumif(arg_count),
|
||||
Function::Sumifs => vec![Signature::Vector; arg_count],
|
||||
Function::Tan => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Tanh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::ErrorType => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isblank => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Iserr => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Iserror => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Iseven => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isformula => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Islogical => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isna => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isnontext => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isnumber => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isodd => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Isref => args_signature_one_vector(arg_count),
|
||||
Function::Istext => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Na => args_signature_no_args(arg_count),
|
||||
Function::Sheet => args_signature_sheet(arg_count),
|
||||
Function::Type => args_signature_one_vector(arg_count),
|
||||
Function::Hlookup => args_signature_hlookup(arg_count),
|
||||
Function::Index => args_signature_index(arg_count),
|
||||
Function::Indirect => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Lookup => args_signature_lookup(arg_count),
|
||||
Function::Match => args_signature_match(arg_count),
|
||||
Function::Offset => args_signature_offset(arg_count),
|
||||
Function::Row => args_signature_row(arg_count),
|
||||
Function::Rows => args_signature_one_vector(arg_count),
|
||||
Function::Vlookup => args_signature_hlookup(arg_count),
|
||||
Function::Xlookup => args_signature_xlookup(arg_count),
|
||||
Function::Concat => vec![Signature::Vector; arg_count],
|
||||
Function::Concatenate => vec![Signature::Scalar; arg_count],
|
||||
Function::Exact => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Find => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Left => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Len => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Lower => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Mid => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Rept => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Right => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Search => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Substitute => args_signature_scalars(arg_count, 3, 1),
|
||||
Function::T => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Text => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Textafter => args_signature_textafter(arg_count),
|
||||
Function::Textbefore => args_signature_textafter(arg_count),
|
||||
Function::Textjoin => args_signature_textjoin(arg_count),
|
||||
Function::Trim => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Upper => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Value => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Valuetotext => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Average => vec![Signature::Vector; arg_count],
|
||||
Function::Averagea => vec![Signature::Vector; arg_count],
|
||||
Function::Averageif => args_signature_sumif(arg_count),
|
||||
Function::Averageifs => vec![Signature::Vector; arg_count],
|
||||
Function::Count => vec![Signature::Vector; arg_count],
|
||||
Function::Counta => vec![Signature::Vector; arg_count],
|
||||
Function::Countblank => vec![Signature::Vector; arg_count],
|
||||
Function::Countif => args_signature_sumif(arg_count),
|
||||
Function::Countifs => vec![Signature::Vector; arg_count],
|
||||
Function::Maxifs => vec![Signature::Vector; arg_count],
|
||||
Function::Minifs => vec![Signature::Vector; arg_count],
|
||||
Function::Date => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Day => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Edate => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Eomonth => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Month => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Now => args_signature_no_args(arg_count),
|
||||
Function::Today => args_signature_no_args(arg_count),
|
||||
Function::Year => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Cumipmt => args_signature_scalars(arg_count, 6, 0),
|
||||
Function::Cumprinc => args_signature_scalars(arg_count, 6, 0),
|
||||
Function::Db => args_signature_scalars(arg_count, 4, 1),
|
||||
Function::Ddb => args_signature_scalars(arg_count, 4, 1),
|
||||
Function::Dollarde => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Dollarfr => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Effect => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Fv => args_signature_scalars(arg_count, 3, 2),
|
||||
Function::Ipmt => args_signature_scalars(arg_count, 4, 2),
|
||||
Function::Irr => args_signature_irr(arg_count),
|
||||
Function::Ispmt => args_signature_scalars(arg_count, 4, 0),
|
||||
Function::Mirr => args_signature_mirr(arg_count),
|
||||
Function::Nominal => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Nper => args_signature_scalars(arg_count, 3, 2),
|
||||
Function::Npv => args_signature_npv(arg_count),
|
||||
Function::Pduration => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Pmt => args_signature_scalars(arg_count, 3, 2),
|
||||
Function::Ppmt => args_signature_scalars(arg_count, 4, 2),
|
||||
Function::Pv => args_signature_scalars(arg_count, 3, 2),
|
||||
Function::Rate => args_signature_scalars(arg_count, 3, 3),
|
||||
Function::Rri => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Sln => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Syd => args_signature_scalars(arg_count, 4, 0),
|
||||
Function::Tbilleq => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Tbillprice => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Tbillyield => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Xirr => args_signature_xirr(arg_count),
|
||||
Function::Xnpv => args_signature_xnpv(arg_count),
|
||||
Function::Besseli => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Besselj => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Besselk => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bessely => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Erf => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Erfc => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::ErfcPrecise => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::ErfPrecise => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Bin2dec => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Bin2hex => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Bin2oct => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Dec2Bin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Dec2hex => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Dec2oct => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Hex2bin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Hex2dec => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Hex2oct => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Oct2bin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Oct2dec => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Oct2hex => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Bitand => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bitlshift => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bitor => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bitrshift => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Bitxor => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Complex => args_signature_scalars(arg_count, 2, 1),
|
||||
Function::Imabs => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imaginary => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imargument => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imconjugate => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcos => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcosh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcot => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcsc => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imcsch => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imdiv => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Imexp => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imln => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imlog10 => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imlog2 => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Impower => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Improduct => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Imreal => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsec => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsech => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsin => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsinh => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsqrt => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Imsub => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Imsum => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Imtan => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Convert => args_signature_scalars(arg_count, 3, 0),
|
||||
Function::Delta => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Gestep => args_signature_scalars(arg_count, 1, 1),
|
||||
Function::Subtotal => args_signature_npv(arg_count),
|
||||
Function::Rand => args_signature_no_args(arg_count),
|
||||
Function::Randbetween => args_signature_scalars(arg_count, 2, 0),
|
||||
Function::Formulatext => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Unicode => args_signature_scalars(arg_count, 1, 0),
|
||||
Function::Geomean => vec![Signature::Vector; arg_count],
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the type of the result (Scalar, Array or Range) depending on the arguments
|
||||
fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
|
||||
match kind {
|
||||
Function::And => StaticResult::Scalar,
|
||||
Function::False => StaticResult::Scalar,
|
||||
Function::If => scalar_arguments(args),
|
||||
Function::Iferror => scalar_arguments(args),
|
||||
Function::Ifna => scalar_arguments(args),
|
||||
Function::Ifs => not_implemented(args),
|
||||
Function::Not => StaticResult::Scalar,
|
||||
Function::Or => StaticResult::Scalar,
|
||||
Function::Switch => not_implemented(args),
|
||||
Function::True => StaticResult::Scalar,
|
||||
Function::Xor => StaticResult::Scalar,
|
||||
Function::Abs => scalar_arguments(args),
|
||||
Function::Acos => scalar_arguments(args),
|
||||
Function::Acosh => scalar_arguments(args),
|
||||
Function::Asin => scalar_arguments(args),
|
||||
Function::Asinh => scalar_arguments(args),
|
||||
Function::Atan => scalar_arguments(args),
|
||||
Function::Atan2 => scalar_arguments(args),
|
||||
Function::Atanh => scalar_arguments(args),
|
||||
Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell),
|
||||
Function::Column => not_implemented(args),
|
||||
Function::Columns => not_implemented(args),
|
||||
Function::Cos => scalar_arguments(args),
|
||||
Function::Cosh => scalar_arguments(args),
|
||||
Function::Max => StaticResult::Scalar,
|
||||
Function::Min => StaticResult::Scalar,
|
||||
Function::Pi => StaticResult::Scalar,
|
||||
Function::Power => scalar_arguments(args),
|
||||
Function::Product => not_implemented(args),
|
||||
Function::Round => scalar_arguments(args),
|
||||
Function::Rounddown => scalar_arguments(args),
|
||||
Function::Roundup => scalar_arguments(args),
|
||||
Function::Sin => scalar_arguments(args),
|
||||
Function::Sinh => scalar_arguments(args),
|
||||
Function::Sqrt => scalar_arguments(args),
|
||||
Function::Sqrtpi => StaticResult::Scalar,
|
||||
Function::Sum => StaticResult::Scalar,
|
||||
Function::Sumif => not_implemented(args),
|
||||
Function::Sumifs => not_implemented(args),
|
||||
Function::Tan => scalar_arguments(args),
|
||||
Function::Tanh => scalar_arguments(args),
|
||||
Function::ErrorType => not_implemented(args),
|
||||
Function::Isblank => not_implemented(args),
|
||||
Function::Iserr => not_implemented(args),
|
||||
Function::Iserror => not_implemented(args),
|
||||
Function::Iseven => not_implemented(args),
|
||||
Function::Isformula => not_implemented(args),
|
||||
Function::Islogical => not_implemented(args),
|
||||
Function::Isna => not_implemented(args),
|
||||
Function::Isnontext => not_implemented(args),
|
||||
Function::Isnumber => not_implemented(args),
|
||||
Function::Isodd => not_implemented(args),
|
||||
Function::Isref => not_implemented(args),
|
||||
Function::Istext => not_implemented(args),
|
||||
Function::Na => StaticResult::Scalar,
|
||||
Function::Sheet => StaticResult::Scalar,
|
||||
Function::Type => not_implemented(args),
|
||||
Function::Hlookup => not_implemented(args),
|
||||
Function::Index => static_analysis_index(args),
|
||||
Function::Indirect => static_analysis_indirect(args),
|
||||
Function::Lookup => not_implemented(args),
|
||||
Function::Match => not_implemented(args),
|
||||
Function::Offset => static_analysis_offset(args),
|
||||
// FIXME: Row could return an array
|
||||
Function::Row => StaticResult::Scalar,
|
||||
Function::Rows => not_implemented(args),
|
||||
Function::Vlookup => not_implemented(args),
|
||||
Function::Xlookup => not_implemented(args),
|
||||
Function::Concat => not_implemented(args),
|
||||
Function::Concatenate => not_implemented(args),
|
||||
Function::Exact => not_implemented(args),
|
||||
Function::Find => not_implemented(args),
|
||||
Function::Left => not_implemented(args),
|
||||
Function::Len => not_implemented(args),
|
||||
Function::Lower => not_implemented(args),
|
||||
Function::Mid => not_implemented(args),
|
||||
Function::Rept => not_implemented(args),
|
||||
Function::Right => not_implemented(args),
|
||||
Function::Search => not_implemented(args),
|
||||
Function::Substitute => not_implemented(args),
|
||||
Function::T => not_implemented(args),
|
||||
Function::Text => not_implemented(args),
|
||||
Function::Textafter => not_implemented(args),
|
||||
Function::Textbefore => not_implemented(args),
|
||||
Function::Textjoin => not_implemented(args),
|
||||
Function::Trim => not_implemented(args),
|
||||
Function::Unicode => not_implemented(args),
|
||||
Function::Upper => not_implemented(args),
|
||||
Function::Value => not_implemented(args),
|
||||
Function::Valuetotext => not_implemented(args),
|
||||
Function::Average => not_implemented(args),
|
||||
Function::Averagea => not_implemented(args),
|
||||
Function::Averageif => not_implemented(args),
|
||||
Function::Averageifs => not_implemented(args),
|
||||
Function::Count => not_implemented(args),
|
||||
Function::Counta => not_implemented(args),
|
||||
Function::Countblank => not_implemented(args),
|
||||
Function::Countif => not_implemented(args),
|
||||
Function::Countifs => not_implemented(args),
|
||||
Function::Maxifs => not_implemented(args),
|
||||
Function::Minifs => not_implemented(args),
|
||||
Function::Date => not_implemented(args),
|
||||
Function::Day => not_implemented(args),
|
||||
Function::Edate => not_implemented(args),
|
||||
Function::Month => not_implemented(args),
|
||||
Function::Now => not_implemented(args),
|
||||
Function::Today => not_implemented(args),
|
||||
Function::Year => not_implemented(args),
|
||||
Function::Cumipmt => not_implemented(args),
|
||||
Function::Cumprinc => not_implemented(args),
|
||||
Function::Db => not_implemented(args),
|
||||
Function::Ddb => not_implemented(args),
|
||||
Function::Dollarde => not_implemented(args),
|
||||
Function::Dollarfr => not_implemented(args),
|
||||
Function::Effect => not_implemented(args),
|
||||
Function::Fv => not_implemented(args),
|
||||
Function::Ipmt => not_implemented(args),
|
||||
Function::Irr => not_implemented(args),
|
||||
Function::Ispmt => not_implemented(args),
|
||||
Function::Mirr => not_implemented(args),
|
||||
Function::Nominal => not_implemented(args),
|
||||
Function::Nper => not_implemented(args),
|
||||
Function::Npv => not_implemented(args),
|
||||
Function::Pduration => not_implemented(args),
|
||||
Function::Pmt => not_implemented(args),
|
||||
Function::Ppmt => not_implemented(args),
|
||||
Function::Pv => not_implemented(args),
|
||||
Function::Rate => not_implemented(args),
|
||||
Function::Rri => not_implemented(args),
|
||||
Function::Sln => not_implemented(args),
|
||||
Function::Syd => not_implemented(args),
|
||||
Function::Tbilleq => not_implemented(args),
|
||||
Function::Tbillprice => not_implemented(args),
|
||||
Function::Tbillyield => not_implemented(args),
|
||||
Function::Xirr => not_implemented(args),
|
||||
Function::Xnpv => not_implemented(args),
|
||||
Function::Besseli => scalar_arguments(args),
|
||||
Function::Besselj => scalar_arguments(args),
|
||||
Function::Besselk => scalar_arguments(args),
|
||||
Function::Bessely => scalar_arguments(args),
|
||||
Function::Erf => scalar_arguments(args),
|
||||
Function::Erfc => scalar_arguments(args),
|
||||
Function::ErfcPrecise => scalar_arguments(args),
|
||||
Function::ErfPrecise => scalar_arguments(args),
|
||||
Function::Bin2dec => scalar_arguments(args),
|
||||
Function::Bin2hex => scalar_arguments(args),
|
||||
Function::Bin2oct => scalar_arguments(args),
|
||||
Function::Dec2Bin => scalar_arguments(args),
|
||||
Function::Dec2hex => scalar_arguments(args),
|
||||
Function::Dec2oct => scalar_arguments(args),
|
||||
Function::Hex2bin => scalar_arguments(args),
|
||||
Function::Hex2dec => scalar_arguments(args),
|
||||
Function::Hex2oct => scalar_arguments(args),
|
||||
Function::Oct2bin => scalar_arguments(args),
|
||||
Function::Oct2dec => scalar_arguments(args),
|
||||
Function::Oct2hex => scalar_arguments(args),
|
||||
Function::Bitand => scalar_arguments(args),
|
||||
Function::Bitlshift => scalar_arguments(args),
|
||||
Function::Bitor => scalar_arguments(args),
|
||||
Function::Bitrshift => scalar_arguments(args),
|
||||
Function::Bitxor => scalar_arguments(args),
|
||||
Function::Complex => scalar_arguments(args),
|
||||
Function::Imabs => scalar_arguments(args),
|
||||
Function::Imaginary => scalar_arguments(args),
|
||||
Function::Imargument => scalar_arguments(args),
|
||||
Function::Imconjugate => scalar_arguments(args),
|
||||
Function::Imcos => scalar_arguments(args),
|
||||
Function::Imcosh => scalar_arguments(args),
|
||||
Function::Imcot => scalar_arguments(args),
|
||||
Function::Imcsc => scalar_arguments(args),
|
||||
Function::Imcsch => scalar_arguments(args),
|
||||
Function::Imdiv => scalar_arguments(args),
|
||||
Function::Imexp => scalar_arguments(args),
|
||||
Function::Imln => scalar_arguments(args),
|
||||
Function::Imlog10 => scalar_arguments(args),
|
||||
Function::Imlog2 => scalar_arguments(args),
|
||||
Function::Impower => scalar_arguments(args),
|
||||
Function::Improduct => scalar_arguments(args),
|
||||
Function::Imreal => scalar_arguments(args),
|
||||
Function::Imsec => scalar_arguments(args),
|
||||
Function::Imsech => scalar_arguments(args),
|
||||
Function::Imsin => scalar_arguments(args),
|
||||
Function::Imsinh => scalar_arguments(args),
|
||||
Function::Imsqrt => scalar_arguments(args),
|
||||
Function::Imsub => scalar_arguments(args),
|
||||
Function::Imsum => scalar_arguments(args),
|
||||
Function::Imtan => scalar_arguments(args),
|
||||
Function::Convert => not_implemented(args),
|
||||
Function::Delta => not_implemented(args),
|
||||
Function::Gestep => not_implemented(args),
|
||||
Function::Subtotal => not_implemented(args),
|
||||
Function::Rand => not_implemented(args),
|
||||
Function::Randbetween => scalar_arguments(args),
|
||||
Function::Eomonth => scalar_arguments(args),
|
||||
Function::Formulatext => not_implemented(args),
|
||||
Function::Geomean => not_implemented(args),
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::{super::utils::quote_name, Node, Reference};
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
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};
|
||||
|
||||
@@ -34,10 +35,21 @@ pub enum DisplaceData {
|
||||
None,
|
||||
}
|
||||
|
||||
/// This is the internal mode in IronCalc
|
||||
pub fn to_rc_format(node: &Node) -> String {
|
||||
stringify(node, None, &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// This is the mode used to display the formula in the UI
|
||||
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// This is the mode used to export the formula to Excel
|
||||
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, true)
|
||||
}
|
||||
|
||||
pub fn to_string_displaced(
|
||||
node: &Node,
|
||||
context: &CellReferenceRC,
|
||||
@@ -46,18 +58,10 @@ pub fn to_string_displaced(
|
||||
stringify(node, Some(context), displace_data, false)
|
||||
}
|
||||
|
||||
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, true)
|
||||
}
|
||||
|
||||
/// Converts a local reference to a string applying some displacement if needed.
|
||||
/// It uses A1 style if context is not None. If context is None it uses R1C1 style
|
||||
/// If full_row is true then the row details will be omitted in the A1 case
|
||||
/// If full_colum is true then column details will be omitted.
|
||||
/// If full_column is true then column details will be omitted.
|
||||
pub(crate) fn stringify_reference(
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
@@ -235,7 +239,7 @@ fn format_function(
|
||||
args: &Vec<Node>,
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
use_original_name: bool,
|
||||
export_to_excel: bool,
|
||||
) -> String {
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
@@ -244,11 +248,11 @@ fn format_function(
|
||||
arguments = format!(
|
||||
"{},{}",
|
||||
arguments,
|
||||
stringify(el, context, displace_data, use_original_name)
|
||||
stringify(el, context, displace_data, export_to_excel)
|
||||
);
|
||||
} else {
|
||||
first = false;
|
||||
arguments = stringify(el, context, displace_data, use_original_name);
|
||||
arguments = stringify(el, context, displace_data, export_to_excel);
|
||||
}
|
||||
}
|
||||
format!("{}({})", name, arguments)
|
||||
@@ -258,7 +262,7 @@ fn stringify(
|
||||
node: &Node,
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
use_original_name: bool,
|
||||
export_to_excel: bool,
|
||||
) -> String {
|
||||
use self::Node::*;
|
||||
match node {
|
||||
@@ -407,52 +411,52 @@ fn stringify(
|
||||
}
|
||||
OpRangeKind { left, right } => format!(
|
||||
"{}:{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(left, context, displace_data, export_to_excel),
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpConcatenateKind { left, right } => format!(
|
||||
"{}&{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(left, context, displace_data, export_to_excel),
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
CompareKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(left, context, displace_data, export_to_excel),
|
||||
kind,
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpSumKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(left, context, displace_data, export_to_excel),
|
||||
kind,
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpProductKind { kind, left, right } => {
|
||||
let x = match **left {
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
stringify(left, context, displace_data, export_to_excel)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
stringify(left, context, displace_data, export_to_excel)
|
||||
),
|
||||
_ => stringify(left, context, displace_data, use_original_name),
|
||||
_ => stringify(left, context, displace_data, export_to_excel),
|
||||
};
|
||||
let y = match **right {
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
OpProductKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
_ => stringify(right, context, displace_data, use_original_name),
|
||||
_ => stringify(right, context, displace_data, export_to_excel),
|
||||
};
|
||||
format!("{}{}{}", x, kind, y)
|
||||
}
|
||||
@@ -467,9 +471,7 @@ fn stringify(
|
||||
| DefinedNameKind(_)
|
||||
| TableNameKind(_)
|
||||
| WrongVariableKind(_)
|
||||
| WrongRangeKind { .. } => {
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
}
|
||||
| WrongRangeKind { .. } => stringify(left, context, displace_data, export_to_excel),
|
||||
OpRangeKind { .. }
|
||||
| OpConcatenateKind { .. }
|
||||
| OpProductKind { .. }
|
||||
@@ -482,9 +484,10 @@ fn stringify(
|
||||
| ParseErrorKind { .. }
|
||||
| OpSumKind { .. }
|
||||
| CompareKind { .. }
|
||||
| ImplicitIntersection { .. }
|
||||
| EmptyArgKind => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
stringify(left, context, displace_data, export_to_excel)
|
||||
),
|
||||
};
|
||||
let y = match **right {
|
||||
@@ -498,7 +501,7 @@ fn stringify(
|
||||
| TableNameKind(_)
|
||||
| WrongVariableKind(_)
|
||||
| WrongRangeKind { .. } => {
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
}
|
||||
OpRangeKind { .. }
|
||||
| OpConcatenateKind { .. }
|
||||
@@ -512,23 +515,24 @@ fn stringify(
|
||||
| ParseErrorKind { .. }
|
||||
| OpSumKind { .. }
|
||||
| CompareKind { .. }
|
||||
| ImplicitIntersection { .. }
|
||||
| EmptyArgKind => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
),
|
||||
};
|
||||
format!("{}^{}", x, y)
|
||||
}
|
||||
InvalidFunctionKind { name, args } => {
|
||||
format_function(name, args, context, displace_data, use_original_name)
|
||||
format_function(name, args, context, displace_data, export_to_excel)
|
||||
}
|
||||
FunctionKind { kind, args } => {
|
||||
let name = if use_original_name {
|
||||
let name = if export_to_excel {
|
||||
kind.to_xlsx_string()
|
||||
} else {
|
||||
kind.to_string()
|
||||
};
|
||||
format_function(&name, args, context, displace_data, use_original_name)
|
||||
format_function(&name, args, context, displace_data, export_to_excel)
|
||||
}
|
||||
ArrayKind(args) => {
|
||||
let mut first = true;
|
||||
@@ -538,29 +542,29 @@ fn stringify(
|
||||
arguments = format!(
|
||||
"{},{}",
|
||||
arguments,
|
||||
stringify(el, context, displace_data, use_original_name)
|
||||
stringify(el, context, displace_data, export_to_excel)
|
||||
);
|
||||
} else {
|
||||
first = false;
|
||||
arguments = stringify(el, context, displace_data, use_original_name);
|
||||
arguments = stringify(el, context, displace_data, export_to_excel);
|
||||
}
|
||||
}
|
||||
format!("{{{}}}", arguments)
|
||||
}
|
||||
TableNameKind(value) => value.to_string(),
|
||||
DefinedNameKind((name, _)) => name.to_string(),
|
||||
DefinedNameKind((name, ..)) => name.to_string(),
|
||||
WrongVariableKind(name) => name.to_string(),
|
||||
UnaryKind { kind, right } => match kind {
|
||||
OpUnary::Minus => {
|
||||
format!(
|
||||
"-{}",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
)
|
||||
}
|
||||
OpUnary::Percentage => {
|
||||
format!(
|
||||
"{}%",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
stringify(right, context, displace_data, export_to_excel)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -571,6 +575,29 @@ fn stringify(
|
||||
message: _,
|
||||
} => formula.to_string(),
|
||||
EmptyArgKind => "".to_string(),
|
||||
ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => {
|
||||
if export_to_excel {
|
||||
// We need to check wether the II can be automatic or not
|
||||
let mut new_node = child.as_ref().clone();
|
||||
|
||||
add_implicit_intersection(&mut new_node, true);
|
||||
if matches!(&new_node, Node::ImplicitIntersection { .. }) {
|
||||
return stringify(child, context, displace_data, export_to_excel);
|
||||
}
|
||||
|
||||
return format!(
|
||||
"_xlfn.SINGLE({})",
|
||||
stringify(child, context, displace_data, export_to_excel)
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"@{}",
|
||||
stringify(child, context, displace_data, export_to_excel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,6 +685,12 @@ pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name:
|
||||
Node::UnaryKind { kind: _, right } => {
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => {
|
||||
rename_sheet_in_node(child, sheet_index, new_name);
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
Node::BooleanKind(_) => {}
|
||||
@@ -681,7 +714,7 @@ pub(crate) fn rename_defined_name_in_node(
|
||||
) {
|
||||
match node {
|
||||
// Rename
|
||||
Node::DefinedNameKind((n, s)) => {
|
||||
Node::DefinedNameKind((n, s, _)) => {
|
||||
if name.to_lowercase() == n.to_lowercase() && *s == scope {
|
||||
*n = new_name.to_string();
|
||||
}
|
||||
@@ -736,6 +769,12 @@ pub(crate) fn rename_defined_name_in_node(
|
||||
Node::UnaryKind { kind: _, right } => {
|
||||
rename_defined_name_in_node(right, name, scope, new_name);
|
||||
}
|
||||
Node::ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => {
|
||||
rename_defined_name_in_node(child, name, scope, new_name);
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
Node::BooleanKind(_) => {}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
mod test_add_implicit_intersection;
|
||||
mod test_general;
|
||||
mod test_implicit_intersection;
|
||||
mod test_issue_155;
|
||||
mod test_move_formula;
|
||||
mod test_ranges;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::{
|
||||
parser::{
|
||||
stringify::{to_excel_string, to_string},
|
||||
Parser,
|
||||
},
|
||||
types::CellReferenceRC,
|
||||
};
|
||||
|
||||
use crate::expressions::parser::static_analysis::add_implicit_intersection;
|
||||
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
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 cases = vec![
|
||||
("A1:A10*SUM(A1:A10)", "@A1:A10*SUM(A1:A10)"),
|
||||
("A1:A10", "@A1:A10"),
|
||||
// Math and trigonometry functions
|
||||
("SUM(A1:A10)", "SUM(A1:A10)"),
|
||||
("SIN(A1:A10)", "SIN(@A1:A10)"),
|
||||
("COS(A1:A10)", "COS(@A1:A10)"),
|
||||
("TAN(A1:A10)", "TAN(@A1:A10)"),
|
||||
("ASIN(A1:A10)", "ASIN(@A1:A10)"),
|
||||
("ACOS(A1:A10)", "ACOS(@A1:A10)"),
|
||||
("ATAN(A1:A10)", "ATAN(@A1:A10)"),
|
||||
("SINH(A1:A10)", "SINH(@A1:A10)"),
|
||||
("COSH(A1:A10)", "COSH(@A1:A10)"),
|
||||
("TANH(A1:A10)", "TANH(@A1:A10)"),
|
||||
("ASINH(A1:A10)", "ASINH(@A1:A10)"),
|
||||
("ACOSH(A1:A10)", "ACOSH(@A1:A10)"),
|
||||
("ATANH(A1:A10)", "ATANH(@A1:A10)"),
|
||||
("ATAN2(A1:A10,B1:B10)", "ATAN2(@A1:A10,@B1:B10)"),
|
||||
("ATAN2(A1:A10,A1)", "ATAN2(@A1:A10,A1)"),
|
||||
("SQRT(A1:A10)", "SQRT(@A1:A10)"),
|
||||
("SQRTPI(A1:A10)", "SQRTPI(@A1:A10)"),
|
||||
("POWER(A1:A10,A1)", "POWER(@A1:A10,A1)"),
|
||||
("POWER(A1:A10,B1:B10)", "POWER(@A1:A10,@B1:B10)"),
|
||||
("MAX(A1:A10)", "MAX(A1:A10)"),
|
||||
("MIN(A1:A10)", "MIN(A1:A10)"),
|
||||
("ABS(A1:A10)", "ABS(@A1:A10)"),
|
||||
("FALSE()", "FALSE()"),
|
||||
("TRUE()", "TRUE()"),
|
||||
// Defined names
|
||||
("BADNMAE", "@BADNMAE"),
|
||||
// Logical
|
||||
("AND(A1:A10)", "AND(A1:A10)"),
|
||||
("OR(A1:A10)", "OR(A1:A10)"),
|
||||
("NOT(A1:A10)", "NOT(@A1:A10)"),
|
||||
("IF(A1:A10,B1:B10,C1:C10)", "IF(@A1:A10,@B1:B10,@C1:C10)"),
|
||||
// Information
|
||||
// ("ISBLANK(A1:A10)", "ISBLANK(A1:A10)"),
|
||||
// ("ISERR(A1:A10)", "ISERR(A1:A10)"),
|
||||
// ("ISERROR(A1:A10)", "ISERROR(A1:A10)"),
|
||||
// ("ISEVEN(A1:A10)", "ISEVEN(A1:A10)"),
|
||||
// ("ISLOGICAL(A1:A10)", "ISLOGICAL(A1:A10)"),
|
||||
// ("ISNA(A1:A10)", "ISNA(A1:A10)"),
|
||||
// ("ISNONTEXT(A1:A10)", "ISNONTEXT(A1:A10)"),
|
||||
// ("ISNUMBER(A1:A10)", "ISNUMBER(A1:A10)"),
|
||||
// ("ISODD(A1:A10)", "ISODD(A1:A10)"),
|
||||
// ("ISREF(A1:A10)", "ISREF(A1:A10)"),
|
||||
// ("ISTEXT(A1:A10)", "ISTEXT(A1:A10)"),
|
||||
];
|
||||
for (formula, expected) in cases {
|
||||
let mut t = parser.parse(formula, &cell_reference);
|
||||
add_implicit_intersection(&mut t, true);
|
||||
let r = to_string(&t, &cell_reference);
|
||||
assert_eq!(r, expected);
|
||||
let excel_formula = to_excel_string(&t, &cell_reference);
|
||||
assert_eq!(excel_formula, formula);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#![allow(clippy::panic)]
|
||||
|
||||
use crate::expressions::parser::{Node, Parser};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!B3
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 3,
|
||||
column: 2,
|
||||
};
|
||||
let t = parser.parse("@A1:A10", &cell_reference);
|
||||
let child = Node::RangeKind {
|
||||
sheet_name: None,
|
||||
sheet_index: 0,
|
||||
absolute_row1: false,
|
||||
absolute_column1: false,
|
||||
row1: -2,
|
||||
column1: -1,
|
||||
absolute_row2: false,
|
||||
absolute_column2: false,
|
||||
row2: 7,
|
||||
column2: -1,
|
||||
};
|
||||
assert_eq!(
|
||||
t,
|
||||
Node::ImplicitIntersection {
|
||||
automatic: false,
|
||||
child: Box::new(child)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_add() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!B3
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 3,
|
||||
column: 2,
|
||||
};
|
||||
let t = parser.parse("@A1:A10+12", &cell_reference);
|
||||
let child = Node::RangeKind {
|
||||
sheet_name: None,
|
||||
sheet_index: 0,
|
||||
absolute_row1: false,
|
||||
absolute_column1: false,
|
||||
row1: -2,
|
||||
column1: -1,
|
||||
absolute_row2: false,
|
||||
absolute_column2: false,
|
||||
row2: 7,
|
||||
column2: -1,
|
||||
};
|
||||
assert_eq!(
|
||||
t,
|
||||
Node::OpSumKind {
|
||||
kind: crate::expressions::token::OpSum::Add,
|
||||
left: Box::new(Node::ImplicitIntersection {
|
||||
automatic: false,
|
||||
child: Box::new(child)
|
||||
}),
|
||||
right: Box::new(Node::NumberKind(12.0))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -387,7 +387,7 @@ fn test_move_formula_misc() {
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
let node = parser.parse("X9^C2-F4*H2", context);
|
||||
let node = parser.parse("X9^C2-F4*H2+SUM(F2:H4)+SUM(C2:F6)", context);
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
@@ -400,7 +400,7 @@ fn test_move_formula_misc() {
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "X9^M12-P14*H2");
|
||||
assert_eq!(t, "X9^M12-P14*H2+SUM(F2:H4)+SUM(M12:P16)");
|
||||
|
||||
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", context);
|
||||
let t = move_formula(
|
||||
@@ -475,3 +475,77 @@ fn test_move_formula_another_sheet() {
|
||||
"Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_formula_implicit_intersetion() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)", context);
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_formula_implicit_intersetion_with_ranges() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
let node = parser.parse("SUM(@F2:H4)+SUM(@C2:F6)+SUM(@A1, @X9, @$D$5)", context);
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(@F2:H4)+SUM(@M12:P16)+SUM(@A1,@X9,@$N$15)");
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::utils::{number_to_column, parse_reference_a1};
|
||||
use crate::types::{Table, TableColumn, TableStyleInfo};
|
||||
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use crate::expressions::utils::{number_to_column, parse_reference_a1};
|
||||
use crate::types::{Table, TableColumn, TableStyleInfo};
|
||||
|
||||
fn create_test_table(
|
||||
table_name: &str,
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
use super::{move_formula::ref_is_in_area, Node};
|
||||
|
||||
use crate::expressions::types::{Area, CellReferenceIndex};
|
||||
|
||||
pub(crate) fn forward_references(
|
||||
node: &mut Node,
|
||||
context: &CellReferenceIndex,
|
||||
source_area: &Area,
|
||||
target_sheet: u32,
|
||||
target_sheet_name: &str,
|
||||
target_row: i32,
|
||||
target_column: i32,
|
||||
) {
|
||||
match node {
|
||||
Node::ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index: reference_sheet,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
row: reference_row,
|
||||
column: reference_column,
|
||||
} => {
|
||||
let reference_row_absolute = if *absolute_row {
|
||||
*reference_row
|
||||
} else {
|
||||
*reference_row + context.row
|
||||
};
|
||||
let reference_column_absolute = if *absolute_column {
|
||||
*reference_column
|
||||
} else {
|
||||
*reference_column + context.column
|
||||
};
|
||||
if ref_is_in_area(
|
||||
*reference_sheet,
|
||||
reference_row_absolute,
|
||||
reference_column_absolute,
|
||||
source_area,
|
||||
) {
|
||||
if *reference_sheet != target_sheet {
|
||||
*sheet_name = Some(target_sheet_name.to_string());
|
||||
*reference_sheet = target_sheet;
|
||||
}
|
||||
*reference_row = target_row + *reference_row - source_area.row;
|
||||
*reference_column = target_column + *reference_column - source_area.column;
|
||||
}
|
||||
}
|
||||
Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
let reference_row1 = if *absolute_row1 {
|
||||
*row1
|
||||
} else {
|
||||
*row1 + context.row
|
||||
};
|
||||
let reference_column1 = if *absolute_column1 {
|
||||
*column1
|
||||
} else {
|
||||
*column1 + context.column
|
||||
};
|
||||
|
||||
let reference_row2 = if *absolute_row2 {
|
||||
*row2
|
||||
} else {
|
||||
*row2 + context.row
|
||||
};
|
||||
let reference_column2 = if *absolute_column2 {
|
||||
*column2
|
||||
} else {
|
||||
*column2 + context.column
|
||||
};
|
||||
if ref_is_in_area(*sheet_index, reference_row1, reference_column1, source_area)
|
||||
&& ref_is_in_area(*sheet_index, reference_row2, reference_column2, source_area)
|
||||
{
|
||||
if *sheet_index != target_sheet {
|
||||
*sheet_index = target_sheet;
|
||||
*sheet_name = Some(target_sheet_name.to_string());
|
||||
}
|
||||
*row1 = target_row + *row1 - source_area.row;
|
||||
*column1 = target_column + *column1 - source_area.column;
|
||||
*row2 = target_row + *row2 - source_area.row;
|
||||
*column2 = target_column + *column2 - source_area.column;
|
||||
}
|
||||
}
|
||||
// Recurse
|
||||
Node::OpRangeKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpConcatenateKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpSumKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpProductKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpPowerKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::FunctionKind { kind: _, args } => {
|
||||
for arg in args {
|
||||
forward_references(
|
||||
arg,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
}
|
||||
Node::InvalidFunctionKind { name: _, args } => {
|
||||
for arg in args {
|
||||
forward_references(
|
||||
arg,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
}
|
||||
Node::CompareKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::UnaryKind { kind: _, right } => {
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
// TODO: Not implemented
|
||||
Node::ArrayKind(_) => {}
|
||||
// Do nothing. Note: we could do a blanket _ => {}
|
||||
Node::DefinedNameKind(_) => {}
|
||||
Node::TableNameKind(_) => {}
|
||||
Node::WrongVariableKind(_) => {}
|
||||
Node::ErrorKind(_) => {}
|
||||
Node::ParseErrorKind { .. } => {}
|
||||
Node::EmptyArgKind => {}
|
||||
Node::BooleanKind(_) => {}
|
||||
Node::NumberKind(_) => {}
|
||||
Node::StringKind(_) => {}
|
||||
Node::WrongReferenceKind { .. } => {}
|
||||
Node::WrongRangeKind { .. } => {}
|
||||
}
|
||||
}
|
||||
@@ -240,6 +240,7 @@ pub enum TokenType {
|
||||
Bang, // !
|
||||
Percent, // %
|
||||
And, // &
|
||||
At, // @
|
||||
Reference {
|
||||
sheet: Option<String>,
|
||||
row: i32,
|
||||
|
||||
@@ -249,7 +249,7 @@ impl Model {
|
||||
// The arg could be a defined name or a table
|
||||
// let = &args[0];
|
||||
match &args[0] {
|
||||
Node::DefinedNameKind((name, scope)) => {
|
||||
Node::DefinedNameKind((name, scope, _)) => {
|
||||
// Let's see if it is a defined name
|
||||
if let Some(defined_name) = self
|
||||
.parsed_defined_names
|
||||
|
||||
@@ -855,7 +855,7 @@ impl Model {
|
||||
if left.row != right.row || left.column != right.column {
|
||||
// FIXME: Implicit intersection or dynamic arrays
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "argument must be a reference to a single cell".to_string(),
|
||||
};
|
||||
|
||||
@@ -41,7 +41,6 @@ pub mod worksheet;
|
||||
mod actions;
|
||||
mod cast;
|
||||
mod constants;
|
||||
mod diffs;
|
||||
mod functions;
|
||||
mod implicit_intersection;
|
||||
mod model;
|
||||
|
||||
@@ -207,6 +207,17 @@ impl Model {
|
||||
},
|
||||
}
|
||||
}
|
||||
Node::ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => match self.evaluate_node_with_reference(child, cell) {
|
||||
CalcResult::Range { left, right } => CalcResult::Range { left, right },
|
||||
_ => CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
format!("Error with Implicit Intersection in cell {:?}", cell),
|
||||
),
|
||||
},
|
||||
_ => self.evaluate_node_in_context(node, cell),
|
||||
}
|
||||
}
|
||||
@@ -416,7 +427,7 @@ impl Model {
|
||||
// TODO: NOT IMPLEMENTED
|
||||
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
|
||||
}
|
||||
DefinedNameKind((name, scope)) => {
|
||||
DefinedNameKind((name, scope, _)) => {
|
||||
if let Ok(Some(parsed_defined_name)) = self.get_parsed_defined_name(name, *scope) {
|
||||
match parsed_defined_name {
|
||||
ParsedDefinedName::CellReference(reference) => {
|
||||
@@ -528,6 +539,22 @@ impl Model {
|
||||
format!("Error parsing {}: {}", formula, message),
|
||||
),
|
||||
EmptyArgKind => CalcResult::EmptyArg,
|
||||
ImplicitIntersection {
|
||||
automatic: _,
|
||||
child,
|
||||
} => match self.evaluate_node_with_reference(child, cell) {
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => self.evaluate_cell(cell_reference),
|
||||
None => CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Error with Implicit Intersection in cell {:?}", cell),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => self.evaluate_node_in_context(child, cell),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,12 +644,15 @@ impl Model {
|
||||
};
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
let range = Range {
|
||||
left: *left,
|
||||
right: *right,
|
||||
};
|
||||
if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
|
||||
if left.sheet == right.sheet
|
||||
&& left.row == right.row
|
||||
&& left.column == right.column
|
||||
{
|
||||
let intersection_cell = CellReferenceIndex {
|
||||
sheet: left.sheet,
|
||||
column: left.column,
|
||||
row: left.row,
|
||||
};
|
||||
let v = self.evaluate_cell(intersection_cell);
|
||||
self.set_cell_value(cell_reference, &v);
|
||||
} else {
|
||||
@@ -639,10 +669,32 @@ impl Model {
|
||||
f,
|
||||
s,
|
||||
o,
|
||||
m: "Invalid reference".to_string(),
|
||||
ei: Error::VALUE,
|
||||
m: "Implicit Intersection not implemented".to_string(),
|
||||
ei: Error::NIMPL,
|
||||
};
|
||||
}
|
||||
// if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
|
||||
// {
|
||||
// let v = self.evaluate_cell(intersection_cell);
|
||||
// self.set_cell_value(cell_reference, &v);
|
||||
// } else {
|
||||
// let o = match self.cell_reference_to_string(&cell_reference) {
|
||||
// Ok(s) => s,
|
||||
// Err(_) => "".to_string(),
|
||||
// };
|
||||
// *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,
|
||||
// m: "Invalid reference".to_string(),
|
||||
// ei: Error::VALUE,
|
||||
// };
|
||||
// }
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {
|
||||
*self.workbook.worksheets[sheet as usize]
|
||||
@@ -865,11 +917,7 @@ impl Model {
|
||||
|
||||
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
|
||||
|
||||
let defined_names = workbook
|
||||
.get_defined_names_with_scope()
|
||||
.iter()
|
||||
.map(|s| (s.0.to_owned(), s.1))
|
||||
.collect();
|
||||
let defined_names = workbook.get_defined_names_with_scope();
|
||||
// add all tables
|
||||
// let mut tables = Vec::new();
|
||||
// for worksheet in worksheets {
|
||||
|
||||
@@ -144,12 +144,7 @@ impl Model {
|
||||
|
||||
/// Reparses all formulas and defined names
|
||||
pub(crate) fn reset_parsed_structures(&mut self) {
|
||||
let defined_names = self
|
||||
.workbook
|
||||
.get_defined_names_with_scope()
|
||||
.iter()
|
||||
.map(|s| (s.0.to_owned(), s.1))
|
||||
.collect();
|
||||
let defined_names = self.workbook.get_defined_names_with_scope();
|
||||
self.parser
|
||||
.set_worksheets_and_names(self.workbook.get_worksheet_names(), defined_names);
|
||||
self.parsed_formulas = vec![];
|
||||
|
||||
@@ -28,7 +28,6 @@ mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_fn_unicode;
|
||||
mod test_forward_references;
|
||||
mod test_frozen_rows_columns;
|
||||
mod test_general;
|
||||
mod test_math;
|
||||
@@ -59,6 +58,7 @@ mod test_fn_type;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_geomean;
|
||||
mod test_get_cell_content;
|
||||
mod test_implicit_intersection;
|
||||
mod test_issue_155;
|
||||
mod test_percentage;
|
||||
mod test_set_functions_error_handling;
|
||||
|
||||
@@ -22,13 +22,14 @@ fn fn_concatenate() {
|
||||
model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#);
|
||||
// This will break once we implement the implicit intersection operator
|
||||
// It should be:
|
||||
// model._set("B2", r#"=CONCATENATE(@A1:A3, "!")"#);
|
||||
model._set("C2", r#"=CONCATENATE(@A1:A3, "!")"#);
|
||||
model._set("B2", r#"=CONCATENATE(A1:A3, "!")"#);
|
||||
model._set("B3", r#"=CONCAT(A1:A3, "!")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"Hello my World!");
|
||||
assert_eq!(model._get_text("B2"), *" my !");
|
||||
assert_eq!(model._get_text("B2"), *"#N/IMPL!");
|
||||
assert_eq!(model._get_text("B3"), *"Hello my World!");
|
||||
assert_eq!(model._get_text("C2"), *" my !");
|
||||
}
|
||||
|
||||
@@ -30,8 +30,18 @@ fn implicit_intersection() {
|
||||
model._set("A2", "=FORMULATEXT(D1:E1)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A2"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("A1"), *"#N/IMPL!");
|
||||
assert_eq!(model._get_text("A2"), *"#N/IMPL!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_intersection_operator() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=1 + 2");
|
||||
model._set("B1", "=FORMULATEXT(@A:A)");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#N/IMPL!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::expressions::types::{Area, CellReferenceIndex};
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_forward_references() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// test single ref changed nd not changed
|
||||
model._set("H8", "=F6*G9");
|
||||
// tests areas
|
||||
model._set("H9", "=SUM(D4:F6)");
|
||||
// absolute coordinates
|
||||
model._set("H10", "=$F$6");
|
||||
// area larger than the source area
|
||||
model._set("H11", "=SUM(D3:F6)");
|
||||
// Test arguments and concat
|
||||
model._set("H12", "=SUM(F6, D4:F6) & D4");
|
||||
// Test range operator. This is syntax error for now.
|
||||
// model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))");
|
||||
// Test operations
|
||||
model._set("H14", "=-D4+D5*F6/F5");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Source Area is D4:F6
|
||||
let source_area = &Area {
|
||||
sheet: 0,
|
||||
row: 4,
|
||||
column: 4,
|
||||
width: 3,
|
||||
height: 3,
|
||||
};
|
||||
|
||||
// We paste in B10
|
||||
let target_row = 10;
|
||||
let target_column = 2;
|
||||
let result = model.forward_references(
|
||||
source_area,
|
||||
&CellReferenceIndex {
|
||||
sheet: 0,
|
||||
row: target_row,
|
||||
column: target_column,
|
||||
},
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_formula("H8"), "=D12*G9");
|
||||
assert_eq!(model._get_formula("H9"), "=SUM(B10:D12)");
|
||||
assert_eq!(model._get_formula("H10"), "=$D$12");
|
||||
|
||||
assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)");
|
||||
assert_eq!(model._get_formula("H12"), "=SUM(D12,B10:D12)&B10");
|
||||
// assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))");
|
||||
assert_eq!(model._get_formula("H14"), "=-B10+B11*D12/D11");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_sheet() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
// test single ref changed not changed
|
||||
model._set("H8", "=F6*G9");
|
||||
// tests areas
|
||||
model._set("H9", "=SUM(D4:F6)");
|
||||
// absolute coordinates
|
||||
model._set("H10", "=$F$6");
|
||||
// area larger than the source area
|
||||
model._set("H11", "=SUM(D3:F6)");
|
||||
// Test arguments and concat
|
||||
model._set("H12", "=SUM(F6, D4:F6) & D4");
|
||||
// Test range operator. This is syntax error for now.
|
||||
// model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))");
|
||||
// Test operations
|
||||
model._set("H14", "=-D4+D5*F6/F5");
|
||||
|
||||
// Adds a new sheet
|
||||
assert!(model.add_sheet("Sheet2").is_ok());
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Source Area is D4:F6
|
||||
let source_area = &Area {
|
||||
sheet: 0,
|
||||
row: 4,
|
||||
column: 4,
|
||||
width: 3,
|
||||
height: 3,
|
||||
};
|
||||
|
||||
// We paste in Sheet2!B10
|
||||
let target_row = 10;
|
||||
let target_column = 2;
|
||||
let result = model.forward_references(
|
||||
source_area,
|
||||
&CellReferenceIndex {
|
||||
sheet: 1,
|
||||
row: target_row,
|
||||
column: target_column,
|
||||
},
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_formula("H8"), "=Sheet2!D12*G9");
|
||||
assert_eq!(model._get_formula("H9"), "=SUM(Sheet2!B10:D12)");
|
||||
assert_eq!(model._get_formula("H10"), "=Sheet2!$D$12");
|
||||
|
||||
assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)");
|
||||
assert_eq!(
|
||||
model._get_formula("H12"),
|
||||
"=SUM(Sheet2!D12,Sheet2!B10:D12)&Sheet2!B10"
|
||||
);
|
||||
// assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))");
|
||||
assert_eq!(
|
||||
model._get_formula("H14"),
|
||||
"=-Sheet2!B10+Sheet2!B11*Sheet2!D12/Sheet2!D11"
|
||||
);
|
||||
}
|
||||
50
base/src/test/test_implicit_intersection.rs
Normal file
50
base/src/test/test_implicit_intersection.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn simple_colum() {
|
||||
let mut model = new_empty_model();
|
||||
// We populate cells A1 to A3
|
||||
model._set("A1", "1");
|
||||
model._set("A2", "2");
|
||||
model._set("A3", "3");
|
||||
|
||||
model._set("C2", "=@A1:A3");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("C2"), "2".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_of_array_is_n_impl() {
|
||||
let mut model = new_empty_model();
|
||||
// We populate cells A1 to A3
|
||||
model._set("A1", "1");
|
||||
model._set("A2", "2");
|
||||
model._set("A3", "3");
|
||||
|
||||
model._set("C2", "=A1:A3");
|
||||
model._set("D2", "=SUM(SIN(A:A)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("C2"), "#N/IMPL!".to_string());
|
||||
assert_eq!(model._get_text("D2"), "#N/IMPL!".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concat() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=CONCAT(@B1:B3)");
|
||||
model._set("A2", "=CONCAT(B1:B3)");
|
||||
model._set("B1", "Hello");
|
||||
model._set("B2", " ");
|
||||
model._set("B3", "world!");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"Hello");
|
||||
assert_eq!(model._get_text("A2"), *"Hello world!");
|
||||
}
|
||||
@@ -299,6 +299,7 @@ impl Model {
|
||||
Node::WrongVariableKind(_) => None,
|
||||
Node::CompareKind { .. } => None,
|
||||
Node::OpPowerKind { .. } => None,
|
||||
Node::ImplicitIntersection { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::vec::Vec;
|
||||
|
||||
use crate::types::*;
|
||||
use crate::{expressions::parser::DefinedNameS, types::*};
|
||||
|
||||
impl Workbook {
|
||||
pub fn get_worksheet_names(&self) -> Vec<String> {
|
||||
@@ -29,7 +29,7 @@ impl Workbook {
|
||||
}
|
||||
|
||||
/// Returns the a list of defined names in the workbook with their scope
|
||||
pub fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>, String)> {
|
||||
pub fn get_defined_names_with_scope(&self) -> Vec<DefinedNameS> {
|
||||
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
|
||||
|
||||
let defined_names = self
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use ironcalc_base::expressions::parser::static_analysis::add_implicit_intersection;
|
||||
use std::{collections::HashMap, io::Read, num::ParseIntError};
|
||||
|
||||
use ironcalc_base::{
|
||||
expressions::{
|
||||
parser::{stringify::to_rc_format, Parser},
|
||||
parser::{stringify::to_rc_format, DefinedNameS, Parser},
|
||||
token::{get_error_by_english_name, Error},
|
||||
types::CellReferenceRC,
|
||||
utils::{column_to_number, parse_reference_a1},
|
||||
@@ -42,7 +43,7 @@ pub(crate) struct Relationship {
|
||||
}
|
||||
|
||||
impl WorkbookXML {
|
||||
fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>)> {
|
||||
fn get_defined_names_with_scope(&self) -> Vec<DefinedNameS> {
|
||||
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
|
||||
|
||||
let defined_names = self
|
||||
@@ -58,7 +59,7 @@ impl WorkbookXML {
|
||||
// convert Option<usize> to Option<u32>
|
||||
.map(|pos| pos as u32);
|
||||
|
||||
(dn.name.clone(), index)
|
||||
(dn.name.clone(), index, dn.formula.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
defined_names
|
||||
@@ -304,12 +305,14 @@ fn from_a1_to_rc(
|
||||
worksheets: &[String],
|
||||
context: String,
|
||||
tables: HashMap<String, Table>,
|
||||
defined_names: Vec<(String, Option<u32>)>,
|
||||
defined_names: Vec<DefinedNameS>,
|
||||
) -> Result<String, XlsxError> {
|
||||
let mut parser = Parser::new(worksheets.to_owned(), defined_names, tables);
|
||||
let cell_reference =
|
||||
parse_reference(&context).map_err(|error| XlsxError::Xml(error.to_string()))?;
|
||||
let t = parser.parse(&formula, &cell_reference);
|
||||
let mut t = parser.parse(&formula, &cell_reference);
|
||||
add_implicit_intersection(&mut t, true);
|
||||
|
||||
Ok(to_rc_format(&t))
|
||||
}
|
||||
|
||||
@@ -706,7 +709,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
|
||||
worksheets: &[String],
|
||||
tables: &HashMap<String, Table>,
|
||||
shared_strings: &mut Vec<String>,
|
||||
defined_names: Vec<(String, Option<u32>)>,
|
||||
defined_names: Vec<DefinedNameS>,
|
||||
) -> Result<(Worksheet, bool), XlsxError> {
|
||||
let sheet_name = &settings.name;
|
||||
let sheet_id = settings.id;
|
||||
|
||||
@@ -48,8 +48,8 @@ fn test_example() {
|
||||
assert_eq!(ws[0].views[&0].range, [13, 5, 20, 14]);
|
||||
|
||||
let model2 = load_from_icalc("tests/example.ic").unwrap();
|
||||
let s = bitcode::encode(&model2.workbook);
|
||||
assert_eq!(workbook, model2.workbook, "{:?}", s);
|
||||
let _ = bitcode::encode(&model2.workbook);
|
||||
assert_eq!(workbook, model2.workbook);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -346,21 +346,31 @@ fn test_xlsx() {
|
||||
let path = format!("{}", Uuid::new_v4());
|
||||
let dir = temp_folder.join(path);
|
||||
fs::create_dir(&dir).unwrap();
|
||||
let mut is_error = false;
|
||||
for file_path in entries {
|
||||
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
|
||||
let file_path_str = file_path.to_str().unwrap();
|
||||
println!("Testing file: {}", file_path_str);
|
||||
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
|
||||
if let Err(message) = test_file(file_path_str) {
|
||||
println!("Error with file: '{file_path_str}'");
|
||||
println!("{}", message);
|
||||
panic!("Model was evaluated inconsistently with XLSX data.")
|
||||
is_error = true;
|
||||
}
|
||||
let t = test_load_and_saving(file_path_str, &dir);
|
||||
if t.is_err() {
|
||||
println!("Error while load and saving file: {file_path_str}");
|
||||
is_error = true;
|
||||
}
|
||||
assert!(test_load_and_saving(file_path_str, &dir).is_ok());
|
||||
} else {
|
||||
println!("skipping");
|
||||
}
|
||||
}
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
assert!(
|
||||
!is_error,
|
||||
"Models were evaluated inconsistently with XLSX data."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -375,20 +385,26 @@ fn no_export() {
|
||||
let path = format!("{}", Uuid::new_v4());
|
||||
let dir = temp_folder.join(path);
|
||||
fs::create_dir(&dir).unwrap();
|
||||
let mut is_error = false;
|
||||
for file_path in entries {
|
||||
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
|
||||
let file_path_str = file_path.to_str().unwrap();
|
||||
println!("Testing file: {}", file_path_str);
|
||||
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
|
||||
if let Err(message) = test_file(file_path_str) {
|
||||
println!("Error with file: '{file_path_str}'");
|
||||
println!("{}", message);
|
||||
panic!("Model was evaluated inconsistently with XLSX data.")
|
||||
is_error = true;
|
||||
}
|
||||
} else {
|
||||
println!("skipping");
|
||||
}
|
||||
}
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
assert!(
|
||||
!is_error,
|
||||
"Models were evaluated inconsistently with XLSX data."
|
||||
);
|
||||
}
|
||||
|
||||
// This test verifies whether exporting the merged cells functionality is happening properly or not.
|
||||
@@ -476,6 +492,7 @@ fn test_documentation_xlsx() {
|
||||
let path = format!("{}", Uuid::new_v4());
|
||||
let dir = temp_folder.join(path);
|
||||
fs::create_dir(&dir).unwrap();
|
||||
let mut is_error = false;
|
||||
for file_path in entries {
|
||||
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
|
||||
let file_path_str = file_path.to_str().unwrap();
|
||||
@@ -487,7 +504,7 @@ fn test_documentation_xlsx() {
|
||||
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
|
||||
if let Err(message) = test_file(file_path_str) {
|
||||
println!("{}", message);
|
||||
panic!("Model was evaluated inconsistently with XLSX data.")
|
||||
is_error = true;
|
||||
}
|
||||
assert!(test_load_and_saving(file_path_str, &dir).is_ok());
|
||||
} else {
|
||||
@@ -495,6 +512,10 @@ fn test_documentation_xlsx() {
|
||||
}
|
||||
}
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
assert!(
|
||||
!is_error,
|
||||
"Models were evaluated inconsistently with XLSX data."
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user