diff --git a/base/src/actions.rs b/base/src/actions.rs index 30c4029..8f72ccb 100644 --- a/base/src/actions.rs +++ b/base/src/actions.rs @@ -22,7 +22,7 @@ impl Model { .cell(row, column) .and_then(|c| c.get_formula()) { - let node = &self.parsed_formulas[sheet as usize][f as usize].clone(); + let node = &self.parsed_formulas[sheet as usize][f as usize].0.clone(); let cell_reference = CellReferenceRC { sheet: self.workbook.worksheets[sheet as usize].get_name(), row, diff --git a/base/src/arithmetic.rs b/base/src/arithmetic.rs index 26f729d..cd9d529 100644 --- a/base/src/arithmetic.rs +++ b/base/src/arithmetic.rs @@ -77,8 +77,6 @@ impl Model { match to_f64(&node) { Ok(f2) => match op(f1, f2) { Ok(x) => data_row.push(ArrayNode::Number(x)), - Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)), - Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)), Err(e) => data_row.push(ArrayNode::Error(e)), }, Err(err) => data_row.push(ArrayNode::Error(err)), @@ -100,8 +98,6 @@ impl Model { match to_f64(&node) { Ok(f1) => match op(f1, f2) { Ok(x) => data_row.push(ArrayNode::Number(x)), - Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)), - Err(Error::VALUE) => data_row.push(ArrayNode::Error(Error::VALUE)), Err(e) => data_row.push(ArrayNode::Error(e)), }, Err(err) => data_row.push(ArrayNode::Error(err)), @@ -137,10 +133,6 @@ impl Model { (Some(v1), Some(v2)) => match (to_f64(v1), to_f64(v2)) { (Ok(f1), Ok(f2)) => match op(f1, f2) { Ok(x) => data_row.push(ArrayNode::Number(x)), - Err(Error::DIV) => data_row.push(ArrayNode::Error(Error::DIV)), - Err(Error::VALUE) => { - data_row.push(ArrayNode::Error(Error::VALUE)) - } Err(e) => data_row.push(ArrayNode::Error(e)), }, (Err(e), _) | (_, Err(e)) => data_row.push(ArrayNode::Error(e)), diff --git a/base/src/cell.rs b/base/src/cell.rs index 70299f1..3fbbf4e 100644 --- a/base/src/cell.rs +++ b/base/src/cell.rs @@ -64,12 +64,50 @@ impl Cell { /// Returns the formula of a cell if any. pub fn get_formula(&self) -> Option { match self { - Cell::CellFormula { f, .. } => Some(*f), - Cell::CellFormulaBoolean { f, .. } => Some(*f), - Cell::CellFormulaNumber { f, .. } => Some(*f), - Cell::CellFormulaString { f, .. } => Some(*f), - Cell::CellFormulaError { f, .. } => Some(*f), - _ => None, + Cell::CellFormula { f, .. } + | Cell::CellFormulaBoolean { f, .. } + | Cell::CellFormulaNumber { f, .. } + | Cell::CellFormulaString { f, .. } + | Cell::CellFormulaError { f, .. } + | Cell::DynamicCellFormula { f, .. } + | Cell::DynamicCellFormulaBoolean { f, .. } + | Cell::DynamicCellFormulaNumber { f, .. } + | Cell::DynamicCellFormulaString { f, .. } + | Cell::DynamicCellFormulaError { f, .. } => Some(*f), + Cell::EmptyCell { .. } + | Cell::BooleanCell { .. } + | Cell::NumberCell { .. } + | Cell::ErrorCell { .. } + | Cell::SharedString { .. } + | Cell::SpillNumberCell { .. } + | Cell::SpillBooleanCell { .. } + | Cell::SpillErrorCell { .. } + | Cell::SpillStringCell { .. } => None, + } + } + + /// Returns the dynamic range of a cell if any. + pub fn get_dynamic_range(&self) -> Option<(i32, i32)> { + match self { + Cell::DynamicCellFormula { r, .. } => Some(*r), + Cell::DynamicCellFormulaBoolean { r, .. } => Some(*r), + Cell::DynamicCellFormulaNumber { r, .. } => Some(*r), + Cell::DynamicCellFormulaString { r, .. } => Some(*r), + Cell::DynamicCellFormulaError { r, .. } => Some(*r), + Cell::EmptyCell { .. } + | Cell::BooleanCell { .. } + | Cell::NumberCell { .. } + | Cell::ErrorCell { .. } + | Cell::SharedString { .. } + | Cell::CellFormula { .. } + | Cell::CellFormulaBoolean { .. } + | Cell::CellFormulaNumber { .. } + | Cell::CellFormulaString { .. } + | Cell::CellFormulaError { .. } + | Cell::SpillNumberCell { .. } + | Cell::SpillBooleanCell { .. } + | Cell::SpillErrorCell { .. } + | Cell::SpillStringCell { .. } => None, } } @@ -89,6 +127,15 @@ impl Cell { Cell::CellFormulaNumber { s, .. } => *s = style, Cell::CellFormulaString { s, .. } => *s = style, Cell::CellFormulaError { s, .. } => *s = style, + Cell::SpillBooleanCell { s, .. } => *s = style, + Cell::SpillNumberCell { s, .. } => *s = style, + Cell::SpillStringCell { s, .. } => *s = style, + Cell::SpillErrorCell { s, .. } => *s = style, + Cell::DynamicCellFormula { s, .. } => *s = style, + Cell::DynamicCellFormulaBoolean { s, .. } => *s = style, + Cell::DynamicCellFormulaNumber { s, .. } => *s = style, + Cell::DynamicCellFormulaString { s, .. } => *s = style, + Cell::DynamicCellFormulaError { s, .. } => *s = style, }; } @@ -104,6 +151,15 @@ impl Cell { Cell::CellFormulaNumber { s, .. } => *s, Cell::CellFormulaString { s, .. } => *s, Cell::CellFormulaError { s, .. } => *s, + Cell::SpillBooleanCell { s, .. } => *s, + Cell::SpillNumberCell { s, .. } => *s, + Cell::SpillStringCell { s, .. } => *s, + Cell::SpillErrorCell { s, .. } => *s, + Cell::DynamicCellFormula { s, .. } => *s, + Cell::DynamicCellFormulaBoolean { s, .. } => *s, + Cell::DynamicCellFormulaNumber { s, .. } => *s, + Cell::DynamicCellFormulaString { s, .. } => *s, + Cell::DynamicCellFormulaError { s, .. } => *s, } } @@ -119,6 +175,15 @@ impl Cell { Cell::CellFormulaNumber { .. } => CellType::Number, Cell::CellFormulaString { .. } => CellType::Text, Cell::CellFormulaError { .. } => CellType::ErrorValue, + Cell::SpillBooleanCell { .. } => CellType::LogicalValue, + Cell::SpillNumberCell { .. } => CellType::Number, + Cell::SpillStringCell { .. } => CellType::Text, + Cell::SpillErrorCell { .. } => CellType::ErrorValue, + Cell::DynamicCellFormula { .. } => CellType::Number, + Cell::DynamicCellFormulaBoolean { .. } => CellType::LogicalValue, + Cell::DynamicCellFormulaNumber { .. } => CellType::Number, + Cell::DynamicCellFormulaString { .. } => CellType::Text, + Cell::DynamicCellFormulaError { .. } => CellType::ErrorValue, } } @@ -136,7 +201,7 @@ impl Cell { Cell::EmptyCell { .. } => CellValue::None, Cell::BooleanCell { v, s: _ } => CellValue::Boolean(*v), Cell::NumberCell { v, s: _ } => CellValue::Number(*v), - Cell::ErrorCell { ei, .. } => { + Cell::ErrorCell { ei, .. } | Cell::SpillErrorCell { ei, .. } => { let v = ei.to_localized_error_string(language); CellValue::String(v) } @@ -148,14 +213,25 @@ impl Cell { }; CellValue::String(v) } - Cell::CellFormula { .. } => CellValue::String("#ERROR!".to_string()), - Cell::CellFormulaBoolean { v, .. } => CellValue::Boolean(*v), - Cell::CellFormulaNumber { v, .. } => CellValue::Number(*v), - Cell::CellFormulaString { v, .. } => CellValue::String(v.clone()), - Cell::CellFormulaError { ei, .. } => { + Cell::DynamicCellFormula { .. } | Cell::CellFormula { .. } => { + CellValue::String("#ERROR!".to_string()) + } + Cell::DynamicCellFormulaBoolean { v, .. } | Cell::CellFormulaBoolean { v, .. } => { + CellValue::Boolean(*v) + } + Cell::DynamicCellFormulaNumber { v, .. } | Cell::CellFormulaNumber { v, .. } => { + CellValue::Number(*v) + } + Cell::DynamicCellFormulaString { v, .. } | Cell::CellFormulaString { v, .. } => { + CellValue::String(v.clone()) + } + Cell::DynamicCellFormulaError { ei, .. } | Cell::CellFormulaError { ei, .. } => { let v = ei.to_localized_error_string(language); CellValue::String(v) } + Cell::SpillBooleanCell { v, .. } => CellValue::Boolean(*v), + Cell::SpillNumberCell { v, .. } => CellValue::Number(*v), + Cell::SpillStringCell { v, .. } => CellValue::String(v.clone()), } } diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f1943..731c95a 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -186,7 +186,7 @@ pub fn add_implicit_intersection(node: &mut Node, add: bool) { }; } -pub(crate) enum StaticResult { +pub enum StaticResult { Scalar, Array(i32, i32), Range(i32, i32), @@ -222,7 +222,7 @@ fn static_analysis_op_nodes(left: &Node, right: &Node) -> StaticResult { // * 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 { +pub(crate) fn run_static_analysis_on_node(node: &Node) -> StaticResult { match node { Node::BooleanKind(_) | Node::NumberKind(_) diff --git a/base/src/functions/subtotal.rs b/base/src/functions/subtotal.rs index cd3f49b..5709197 100644 --- a/base/src/functions/subtotal.rs +++ b/base/src/functions/subtotal.rs @@ -96,7 +96,7 @@ impl Model { match cell.get_formula() { Some(f) => { - let node = &self.parsed_formulas[sheet_index as usize][f as usize]; + let node = &self.parsed_formulas[sheet_index as usize][f as usize].0; matches!( node, Node::FunctionKind { diff --git a/base/src/model.rs b/base/src/model.rs index d55e95a..9b61ec6 100644 --- a/base/src/model.rs +++ b/base/src/model.rs @@ -11,8 +11,9 @@ use crate::{ lexer::LexerMode, parser::{ move_formula::{move_formula, MoveContext}, + static_analysis::{run_static_analysis_on_node, StaticResult}, stringify::{rename_defined_name_in_node, to_rc_format, to_string}, - Node, Parser, + ArrayNode, Node, Parser, }, token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary}, types::*, @@ -99,7 +100,7 @@ pub struct Model { /// A Rust internal representation of an Excel workbook pub workbook: Workbook, /// A list of parsed formulas - pub parsed_formulas: Vec>, + pub parsed_formulas: Vec>, /// A list of parsed defined names pub(crate) parsed_defined_names: HashMap<(Option, String), ParsedDefinedName>, /// An optimization to lookup strings faster @@ -522,14 +523,195 @@ impl Model { } Ok(format!("{}!{}{}", sheet.name, column, cell_reference.row)) } + + /// Sets sheet, target_row, target_column, (width, height), &v + #[allow(clippy::too_many_arguments)] + fn set_spill_cell_with_formula_value( + &mut self, + sheet: u32, + row: i32, + column: i32, + r: (i32, i32), + v: &CalcResult, + s: i32, + f: i32, + ) -> Result<(), String> { + let new_cell = match v { + CalcResult::EmptyCell => Cell::DynamicCellFormulaNumber { + f, + v: 0.0, + s, + r, + a: false, + }, + CalcResult::String(v) => Cell::DynamicCellFormulaString { + f, + v: v.clone(), + s, + r, + a: false, + }, + CalcResult::Number(v) => Cell::DynamicCellFormulaNumber { + v: *v, + s, + r, + f, + a: false, + }, + CalcResult::Boolean(b) => Cell::DynamicCellFormulaBoolean { + v: *b, + s, + r, + f, + a: false, + }, + CalcResult::Error { error, .. } => Cell::DynamicCellFormulaError { + ei: error.clone(), + s, + r, + f, + a: false, + o: "".to_string(), + m: "".to_string(), + }, + + // These cannot happen + // FIXME: Maybe the type of get_cell_value should be different + CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => { + Cell::DynamicCellFormulaError { + ei: Error::ERROR, + s, + r, + f, + a: false, + o: "".to_string(), + m: "".to_string(), + } + } + }; + let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data; + + match sheet_data.get_mut(&row) { + Some(column_data) => match column_data.get(&column) { + Some(_cell) => { + column_data.insert(column, new_cell); + } + None => { + column_data.insert(column, new_cell); + } + }, + None => { + let mut column_data = HashMap::new(); + column_data.insert(column, new_cell); + sheet_data.insert(row, column_data); + } + } + Ok(()) + } + + /// Sets a cell with a "spill" value + fn set_spill_cell_with_value( + &mut self, + sheet: u32, + row: i32, + column: i32, + m: (i32, i32), + v: &CalcResult, + ) -> Result<(), String> { + let style_index = self.get_cell_style_index(sheet, row, column)?; + let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) { + self.workbook + .styles + .get_style_without_quote_prefix(style_index)? + } else { + style_index + }; + let new_cell = match v { + CalcResult::EmptyCell => Cell::SpillNumberCell { + v: 0.0, + s: style_index, + m, + }, + CalcResult::String(s) => Cell::SpillStringCell { + v: s.clone(), + s: new_style_index, + m, + }, + CalcResult::Number(f) => Cell::SpillNumberCell { + v: *f, + s: new_style_index, + m, + }, + CalcResult::Boolean(b) => Cell::SpillBooleanCell { + v: *b, + s: new_style_index, + m, + }, + CalcResult::Error { error, .. } => Cell::SpillErrorCell { + ei: error.clone(), + s: style_index, + m, + }, + + // These cannot happen + // FIXME: Maybe the type of get_cell_value should be different + CalcResult::Range { .. } | CalcResult::EmptyArg | CalcResult::Array(_) => { + Cell::SpillErrorCell { + ei: Error::ERROR, + s: style_index, + m, + } + } + }; + let sheet_data = &mut self.workbook.worksheet_mut(sheet)?.sheet_data; + + match sheet_data.get_mut(&row) { + Some(column_data) => match column_data.get(&column) { + Some(_cell) => { + column_data.insert(column, new_cell); + } + None => { + column_data.insert(column, new_cell); + } + }, + None => { + let mut column_data = HashMap::new(); + column_data.insert(column, new_cell); + sheet_data.insert(row, column_data); + } + } + Ok(()) + } + /// Sets `result` in the cell given by `sheet` sheet index, row and column /// Note that will panic if the cell does not exist /// It will do nothing if the cell does not have a formula #[allow(clippy::expect_used)] - fn set_cell_value(&mut self, cell_reference: CellReferenceIndex, result: &CalcResult) { + fn set_cell_value( + &mut self, + cell_reference: CellReferenceIndex, + result: &CalcResult, + ) -> Result<(), String> { let CellReferenceIndex { sheet, column, row } = cell_reference; - let cell = &self.workbook.worksheets[sheet as usize].sheet_data[&row][&column]; + let cell = self + .workbook + .worksheet(sheet)? + .cell(row, column) + .cloned() + .unwrap_or_default(); let s = cell.get_style(); + // If the cell is a dynamic cell we need to delete all the cells in the range + if let Some((width, height)) = cell.get_dynamic_range() { + for r in row..row + height { + for c in column..column + width { + // skip the "mother" cell + if r == row && c == column { + continue; + } + self.cell_clear_contents(sheet, r, c)?; + } + } + } if let Some(f) = cell.get_formula() { match result { CalcResult::Number(value) => { @@ -594,19 +776,138 @@ impl Model { ei: error.clone(), }; } + CalcResult::EmptyCell | CalcResult::EmptyArg => { + *self.workbook.worksheets[sheet as usize] + .sheet_data + .get_mut(&row) + .expect("expected a row") + .get_mut(&column) + .expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 }; + } CalcResult::Range { left, right } => { if left.sheet == right.sheet && left.row == right.row && left.column == right.column { - let intersection_cell = CellReferenceIndex { + // There is only one cell + let single_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); + let v = self.evaluate_cell(single_cell); + self.set_cell_value(cell_reference, &v)?; } else { + // We need to check if all the cells are empty, otherwise we mark the cell as #SPILL! + let mut all_empty = true; + for r in row..=row + right.row - left.row { + for c in column..=column + right.column - left.column { + if r == row && c == column { + continue; + } + if !self.is_empty_cell(sheet, r, c).unwrap_or(false) { + all_empty = false; + break; + } + } + } + if !all_empty { + 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::DynamicCellFormulaError { + f, + s, + o, + m: "Result would spill to non empty cells".to_string(), + ei: Error::SPILL, + r: (1, 1), + a: false, + }; + return Ok(()); + } + // evaluate all the cells in that range + for r in left.row..=right.row { + for c in left.column..=right.column { + let cell_reference = CellReferenceIndex { + sheet: left.sheet, + row: r, + column: c, + }; + // FIXME: We ned to return an error + self.evaluate_cell(cell_reference); + } + } + // now write the result in the target + for r in left.row..=right.row { + let row_delta = r - left.row; + for c in left.column..=right.column { + let column_delta = c - left.column; + // We need to put whatever is in (left.sheet, r, c) in + // (sheet, row + row_delta, column + column_delta) + // But we need to preserve the style + let target_row = row + row_delta; + let target_column = column + column_delta; + let cell = self + .workbook + .worksheet(left.sheet)? + .cell(r, c) + .cloned() + .unwrap_or_default(); + let cell_reference = CellReferenceIndex { + sheet: left.sheet, + row: r, + column: c, + }; + let v = self.get_cell_value(&cell, cell_reference); + if row == target_row && column == target_column { + // let cell_reference = CellReferenceIndex { sheet, row, column }; + // self.set_cell_value(cell_reference, &v); + self.set_spill_cell_with_formula_value( + sheet, + target_row, + target_column, + (right.column - left.column + 1, right.row - left.row + 1), + &v, + s, + f, + )?; + continue; + } + self.set_spill_cell_with_value( + sheet, + target_row, + target_column, + (row, column), + &v, + )?; + } + } + } + } + CalcResult::Array(array) => { + let width = array[0].len() as i32; + let height = array.len() as i32; + // First we check that we don't spill: + let mut all_empty = true; + for r in row..row + height { + for c in column..column + width { + if r == row && c == column { + continue; + } + if !self.is_empty_cell(sheet, r, c).unwrap_or(false) { + all_empty = false; + break; + } + } + } + if !all_empty { let o = match self.cell_reference_to_string(&cell_reference) { Ok(s) => s, Err(_) => "".to_string(), @@ -620,57 +921,65 @@ impl Model { f, s, o, - m: "Implicit Intersection not implemented".to_string(), - ei: Error::NIMPL, + m: "Result would spill to non empty cells".to_string(), + ei: Error::SPILL, }; + return Ok(()); + } + let mut target_row = row; + for data_row in array { + let mut target_column = column; + for value in data_row { + if row == target_row && column == target_column { + // This is the root cell of the dynamic array + let cell_reference = CellReferenceIndex { sheet, row, column }; + let v = match value { + ArrayNode::Boolean(b) => CalcResult::Boolean(*b), + ArrayNode::Number(f) => CalcResult::Number(*f), + ArrayNode::String(s) => CalcResult::String(s.clone()), + ArrayNode::Error(error) => CalcResult::new_error( + error.clone(), + cell_reference, + error.to_localized_error_string(&self.language), + ), + }; + self.set_spill_cell_with_formula_value( + sheet, + target_row, + target_column, + (width, height), + &v, + s, + f, + )?; + target_column += 1; + continue; + } + let v = match value { + ArrayNode::Boolean(b) => CalcResult::Boolean(*b), + ArrayNode::Number(f) => CalcResult::Number(*f), + ArrayNode::String(s) => CalcResult::String(s.clone()), + ArrayNode::Error(error) => CalcResult::new_error( + error.clone(), + cell_reference, + error.to_localized_error_string(&self.language), + ), + }; + self.set_spill_cell_with_value( + sheet, + target_row, + target_column, + (row, column), + &v, + )?; + target_column += 1; + } + target_row += 1; } - // 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] - .sheet_data - .get_mut(&row) - .expect("expected a row") - .get_mut(&column) - .expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 }; - } - CalcResult::Array(_) => { - *self.workbook.worksheets[sheet as usize] - .sheet_data - .get_mut(&row) - .expect("expected a row") - .get_mut(&column) - .expect("expected a column") = Cell::CellFormulaError { - f, - s, - o: "".to_string(), - m: "Arrays not supported yet".to_string(), - ei: Error::NIMPL, - }; } } } + Ok(()) } /// Sets the color of the sheet tab. @@ -714,16 +1023,18 @@ impl Model { Ok(()) } + // EmptyCell, Boolean, Number, String, Error fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult { use Cell::*; match cell { EmptyCell { .. } => CalcResult::EmptyCell, - BooleanCell { v, .. } => CalcResult::Boolean(*v), - NumberCell { v, .. } => CalcResult::Number(*v), - ErrorCell { ei, .. } => { + BooleanCell { v, .. } | SpillBooleanCell { v, .. } => CalcResult::Boolean(*v), + NumberCell { v, .. } | SpillNumberCell { v, .. } => CalcResult::Number(*v), + ErrorCell { ei, .. } | SpillErrorCell { ei, .. } => { let message = ei.to_localized_error_string(&self.language); CalcResult::new_error(ei.clone(), cell_reference, message) } + SpillStringCell { v, .. } => CalcResult::String(v.clone()), SharedString { si, .. } => { if let Some(s) = self.workbook.shared_strings.get(*si as usize) { CalcResult::String(s.clone()) @@ -732,15 +1043,21 @@ impl Model { CalcResult::new_error(Error::ERROR, cell_reference, message) } } - CellFormula { .. } => CalcResult::Error { + DynamicCellFormula { .. } | CellFormula { .. } => CalcResult::Error { error: Error::ERROR, origin: cell_reference, message: "Unevaluated formula".to_string(), }, - CellFormulaBoolean { v, .. } => CalcResult::Boolean(*v), - CellFormulaNumber { v, .. } => CalcResult::Number(*v), - CellFormulaString { v, .. } => CalcResult::String(v.clone()), - CellFormulaError { ei, o, m, .. } => { + DynamicCellFormulaBoolean { v, .. } | CellFormulaBoolean { v, .. } => { + CalcResult::Boolean(*v) + } + DynamicCellFormulaNumber { v, .. } | CellFormulaNumber { v, .. } => { + CalcResult::Number(*v) + } + DynamicCellFormulaString { v, .. } | CellFormulaString { v, .. } => { + CalcResult::String(v.clone()) + } + DynamicCellFormulaError { ei, o, m, .. } | CellFormulaError { ei, o, m, .. } => { if let Some(cell_reference) = self.parse_reference(o) { CalcResult::new_error(ei.clone(), cell_reference, m.clone()) } else { @@ -810,9 +1127,10 @@ impl Model { self.cells.insert(key, CellState::Evaluating); } } - let node = &self.parsed_formulas[cell_reference.sheet as usize][f as usize].clone(); - let result = self.evaluate_node_in_context(node, cell_reference); - self.set_cell_value(cell_reference, &result); + let (node, _static_result) = + &self.parsed_formulas[cell_reference.sheet as usize][f as usize]; + let result = self.evaluate_node_in_context(&node.clone(), cell_reference); + let _ = self.set_cell_value(cell_reference, &result); // mark cell as evaluated self.cells.insert(key, CellState::Evaluated); result @@ -1100,7 +1418,7 @@ impl Model { Some(cell) => match cell.get_formula() { None => cell.get_text(&self.workbook.shared_strings, &self.language), Some(i) => { - let formula = &self.parsed_formulas[sheet as usize][i as usize]; + let formula = &self.parsed_formulas[sheet as usize][i as usize].0; let cell_ref = CellReferenceRC { sheet: self.workbook.worksheets[sheet as usize].get_name(), row: target_row, @@ -1203,7 +1521,8 @@ impl Model { .get(sheet as usize) .ok_or("missing sheet")? .get(formula_index as usize) - .ok_or("missing formula")?; + .ok_or("missing formula")? + .0; let cell_ref = CellReferenceRC { sheet: worksheet.get_name(), row, @@ -1437,6 +1756,25 @@ impl Model { column: i32, value: String, ) -> Result<(), String> { + // We need to check if the cell is part of a dynamic array + let cell = self + .workbook + .worksheet(sheet)? + .cell(row, column) + .cloned() + .unwrap_or_default(); + // If the cell is a dynamic cell we need to delete all the cells in the range + if let Some((width, height)) = cell.get_dynamic_range() { + for r in row..row + height { + for c in column..column + width { + // skip the "mother" cell + if r == row && c == column { + continue; + } + self.cell_clear_contents(sheet, r, c)?; + } + } + } // If value starts with "'" then we force the style to be quote_prefix let style_index = self.get_cell_style_index(sheet, row, column)?; if let Some(new_value) = value.strip_prefix('\'') { @@ -1462,7 +1800,8 @@ impl Model { self.set_cell_with_formula(sheet, row, column, formula, new_style_index)?; // Update the style if needed let cell = CellReferenceIndex { sheet, row, column }; - let parsed_formula = &self.parsed_formulas[sheet as usize][formula_index as usize]; + let parsed_formula = + &self.parsed_formulas[sheet as usize][formula_index as usize].0; if let Some(units) = self.compute_node_units(parsed_formula, &cell) { let new_style_index = self .workbook @@ -1544,6 +1883,7 @@ impl Model { _ => parsed_formula = new_parsed_formula, } } + let static_result = run_static_analysis_on_node(&parsed_formula); let s = to_rc_format(&parsed_formula); let mut formula_index: i32 = -1; @@ -1552,7 +1892,7 @@ impl Model { } if formula_index == -1 { shared_formulas.push(s); - self.parsed_formulas[sheet as usize].push(parsed_formula); + self.parsed_formulas[sheet as usize].push((parsed_formula, static_result)); formula_index = (shared_formulas.len() as i32) - 1; } worksheet.set_cell_with_formula(row, column, formula_index, style)?; @@ -1747,7 +2087,7 @@ impl Model { }; match cell.get_formula() { Some(formula_index) => { - let formula = &self.parsed_formulas[sheet as usize][formula_index as usize]; + let formula = &self.parsed_formulas[sheet as usize][formula_index as usize].0; let cell_ref = CellReferenceRC { sheet: worksheet.get_name(), row, @@ -1818,9 +2158,22 @@ impl Model { /// # } /// ``` pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { - self.workbook - .worksheet_mut(sheet)? - .cell_clear_contents(row, column)?; + // If it has a spill formula we need to delete the contents of all the spilled cells + let worksheet = self.workbook.worksheet_mut(sheet)?; + if let Some(cell) = worksheet.cell(row, column) { + if let Some((width, height)) = cell.get_dynamic_range() { + for r in row..row + height { + for c in column..column + width { + if row == r && column == c { + // we skip the root cell + continue; + } + worksheet.cell_clear_contents(r, c)?; + } + } + } + } + worksheet.cell_clear_contents(row, column)?; Ok(()) } @@ -1845,6 +2198,18 @@ impl Model { /// # } pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { let worksheet = self.workbook.worksheet_mut(sheet)?; + // Delete the contents of spilled cells if any + if let Some(cell) = worksheet.cell(row, column) { + if let Some((width, height)) = cell.get_dynamic_range() { + for r in row..row + height { + for c in column..column + width { + if row == r && c == column { + worksheet.cell_clear_contents(r, c)?; + } + } + } + } + } let sheet_data = &mut worksheet.sheet_data; if let Some(row_data) = sheet_data.get_mut(&row) { diff --git a/base/src/new_empty.rs b/base/src/new_empty.rs index f861914..f91840e 100644 --- a/base/src/new_empty.rs +++ b/base/src/new_empty.rs @@ -8,6 +8,7 @@ use crate::{ expressions::{ lexer::LexerMode, parser::{ + static_analysis::run_static_analysis_on_node, stringify::{rename_sheet_in_node, to_rc_format, to_string}, Parser, }, @@ -94,7 +95,8 @@ impl Model { let mut parse_formula = Vec::new(); for formula in shared_formulas { let t = self.parser.parse(formula, &cell_reference); - parse_formula.push(t); + let static_result = run_static_analysis_on_node(&t); + parse_formula.push((t, static_result)); } self.parsed_formulas.push(parse_formula); } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 4fab1c2..ba3e5ac 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -52,6 +52,7 @@ mod test_fn_offset; mod test_number_format; mod test_arrays; +mod test_dynamic_arrays; mod test_escape_quotes; mod test_extend; mod test_fn_fv; diff --git a/base/src/test/test_dynamic_arrays.rs b/base/src/test/test_dynamic_arrays.rs new file mode 100644 index 0000000..252b5bc --- /dev/null +++ b/base/src/test/test_dynamic_arrays.rs @@ -0,0 +1,50 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn they_spill() { + let mut model = new_empty_model(); + model._set("A1", "42"); + model._set("A2", "5"); + model._set("A3", "7"); + + model._set("B1", "=A1:A3"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"42"); + assert_eq!(model._get_text("B2"), *"5"); + assert_eq!(model._get_text("B3"), *"7"); +} + +#[test] +fn spill_error() { + let mut model = new_empty_model(); + model._set("A1", "42"); + model._set("A2", "5"); + model._set("A3", "7"); + + model._set("B1", "=A1:A3"); + model._set("B2", "4"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"#SPILL!"); + assert_eq!(model._get_text("B2"), *"4"); + assert_eq!(model._get_text("B3"), *""); +} + +#[test] +fn second_evaluation() { + let mut model = new_empty_model(); + model._set("C3", "={1,2,3}"); + model.evaluate(); + + assert_eq!(model._get_text("D3"), "2"); + + model._set("D8", "23"); + model.evaluate(); + + assert_eq!(model._get_text("D3"), "2"); +} diff --git a/base/src/test/test_implicit_intersection.rs b/base/src/test/test_implicit_intersection.rs index 59687c1..24bcb0a 100644 --- a/base/src/test/test_implicit_intersection.rs +++ b/base/src/test/test_implicit_intersection.rs @@ -3,7 +3,7 @@ use crate::test::util::new_empty_model; #[test] -fn simple_colum() { +fn simple_column() { let mut model = new_empty_model(); // We populate cells A1 to A3 model._set("A1", "1"); @@ -30,7 +30,7 @@ fn return_of_array_is_n_impl() { model.evaluate(); - assert_eq!(model._get_text("C2"), "#N/IMPL!".to_string()); + assert_eq!(model._get_text("C2"), "1".to_string()); assert_eq!(model._get_text("D2"), "1.89188842".to_string()); } diff --git a/base/src/test/user_model/mod.rs b/base/src/test/user_model/mod.rs index 89d3464..0283890 100644 --- a/base/src/test/user_model/mod.rs +++ b/base/src/test/user_model/mod.rs @@ -6,8 +6,10 @@ mod test_border; mod test_clear_cells; mod test_column_style; mod test_defined_names; +mod test_delete_evaluates; mod test_delete_row_column_formatting; mod test_diff_queue; +mod test_dynamic_array; mod test_evaluation; mod test_general; mod test_grid_lines; diff --git a/base/src/test/user_model/test_delete_evaluates.rs b/base/src/test/user_model/test_delete_evaluates.rs new file mode 100644 index 0000000..981d8cc --- /dev/null +++ b/base/src/test/user_model/test_delete_evaluates.rs @@ -0,0 +1,47 @@ +#![allow(clippy::unwrap_used)] + +use crate::{expressions::types::Area, UserModel}; + +#[test] +fn clear_cell_contents_evaluates() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.set_user_input(0, 1, 1, "42").unwrap(); + model.set_user_input(0, 1, 2, "=A1").unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 2), + Ok("42".to_string()) + ); + model + .range_clear_contents(&Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }) + .unwrap(); + + assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("0".to_string())); +} + +#[test] +fn clear_cell_all_evaluates() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.set_user_input(0, 1, 1, "42").unwrap(); + model.set_user_input(0, 1, 2, "=A1").unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 2), + Ok("42".to_string()) + ); + model + .range_clear_all(&Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }) + .unwrap(); + + assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("0".to_string())); +} diff --git a/base/src/test/user_model/test_dynamic_array.rs b/base/src/test/user_model/test_dynamic_array.rs new file mode 100644 index 0000000..351d177 --- /dev/null +++ b/base/src/test/user_model/test_dynamic_array.rs @@ -0,0 +1,130 @@ +#![allow(clippy::unwrap_used)] + +use crate::{expressions::types::Area, UserModel}; + +// Tests basic behavour. +#[test] +fn basic() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + // We put a value by the dynamic array to check the border conditions + model.set_user_input(0, 2, 1, "22").unwrap(); + model.set_user_input(0, 1, 1, "={34,35,3}").unwrap(); + + assert_eq!( + model.get_formatted_cell_value(0, 1, 1), + Ok("34".to_string()) + ); +} + +// Test that overwriting a dynamic array with a single value dissolves the array +#[test] +fn sett_user_input_mother() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.set_user_input(0, 1, 1, "={34,35,3}").unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 2), + Ok("35".to_string()) + ); + model.set_user_input(0, 1, 1, "123").unwrap(); + assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("".to_string())); +} + +#[test] +fn set_user_input_sibling() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.set_user_input(0, 1, 1, "={43,55,34}").unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 2), + Ok("55".to_string()) + ); + // This does nothing + model.set_user_input(0, 1, 2, "123").unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 2), + Ok("55".to_string()) + ); +} + +#[test] +fn basic_undo_redo() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.set_user_input(0, 1, 1, "={34,35,3}").unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 2), + Ok("35".to_string()) + ); + model.undo().unwrap(); + assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("".to_string())); + model.redo().unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 2), + Ok("35".to_string()) + ); +} + +#[test] +fn mixed_spills() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + // D9 => ={1,2,3} + model.set_user_input(0, 9, 4, "={34,35,3}").unwrap(); + // F6 => ={1;2;3;4} + model.set_user_input(0, 6, 6, "={1;2;3;4}").unwrap(); + + // F6 should be #SPILL! + assert_eq!( + model.get_formatted_cell_value(0, 6, 6), + Ok("#SPILL!".to_string()) + ); + + // We delete D9 + model + .range_clear_contents(&Area { + sheet: 0, + row: 9, + column: 4, + width: 1, + height: 1, + }) + .unwrap(); + + // F6 should be 1 + assert_eq!(model.get_formatted_cell_value(0, 6, 6), Ok("1".to_string())); + + // Now we undo that + model.undo().unwrap(); + // F6 should be #SPILL! + assert_eq!( + model.get_formatted_cell_value(0, 6, 6), + Ok("#SPILL!".to_string()) + ); +} + +#[test] +fn spill_order_d9_f6() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + // D9 => ={1,2,3} + model.set_user_input(0, 9, 4, "={34,35,3}").unwrap(); + // F6 => ={1;2;3;4} + model.set_user_input(0, 6, 6, "={1;2;3;4}").unwrap(); + + // F6 should be #SPILL! + assert_eq!( + model.get_formatted_cell_value(0, 6, 6), + Ok("#SPILL!".to_string()) + ); +} + +#[test] +fn spill_order_f6_d9() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + // F6 => ={1;2;3;4} + model.set_user_input(0, 6, 6, "={1;2;3;4}").unwrap(); + // D9 => ={1,2,3} + model.set_user_input(0, 9, 4, "={34,35,3}").unwrap(); + + // D9 should be #SPILL! + assert_eq!( + model.get_formatted_cell_value(0, 9, 4), + Ok("#SPILL!".to_string()) + ); +} diff --git a/base/src/types.rs b/base/src/types.rs index 07b41bc..00e9d7e 100644 --- a/base/src/types.rs +++ b/base/src/types.rs @@ -159,17 +159,17 @@ pub enum CellType { CompoundData = 128, } +/// Cell types +/// s is always the style index of the cell #[derive(Encode, Decode, Debug, Clone, PartialEq)] pub enum Cell { EmptyCell { s: i32, }, - BooleanCell { v: bool, s: i32, }, - NumberCell { v: f64, s: i32, @@ -181,6 +181,7 @@ pub enum Cell { }, // Always a shared string SharedString { + // string index si: i32, s: i32, }, @@ -189,13 +190,11 @@ pub enum Cell { f: i32, s: i32, }, - CellFormulaBoolean { f: i32, v: bool, s: i32, }, - CellFormulaNumber { f: i32, v: f64, @@ -207,9 +206,9 @@ pub enum Cell { v: String, s: i32, }, - CellFormulaError { f: i32, + // error index ei: Error, s: i32, // Origin: Sheet3!C4 @@ -217,7 +216,81 @@ pub enum Cell { // Error Message: "Not implemented function" m: String, }, - // TODO: Array formulas + // All Spill/dynamic cells have a boolean, a for array, if true it is an array formula + // Spill cells point to a mother cell (row, column) + SpillNumberCell { + v: f64, + s: i32, + // mother cell (row, column) + m: (i32, i32), + }, + SpillBooleanCell { + v: bool, + s: i32, + // mother cell (row, column) + m: (i32, i32), + }, + SpillErrorCell { + ei: Error, + s: i32, + // mother cell (row, column) + m: (i32, i32), + }, + SpillStringCell { + v: String, + s: i32, + // mother cell (row, column) + m: (i32, i32), + }, + // Dynamic cell formulas have a range (width, height) + DynamicCellFormula { + f: i32, + s: i32, + // range of the formula (width, height) + r: (i32, i32), + // true if the formula is a CSE formula + a: bool, + }, + DynamicCellFormulaBoolean { + f: i32, + v: bool, + s: i32, + // range of the formula (width, height) + r: (i32, i32), + // true if the formula is a CSE formula + a: bool, + }, + DynamicCellFormulaNumber { + f: i32, + v: f64, + s: i32, + // range of the formula (width, height) + r: (i32, i32), + // true if the formula is a CSE formula + a: bool, + }, + DynamicCellFormulaString { + f: i32, + v: String, + s: i32, + // range of the formula (width, height) + r: (i32, i32), + // true if the formula is a CSE formula + a: bool, + }, + DynamicCellFormulaError { + f: i32, + ei: Error, + s: i32, + // Cell origin of the error + o: String, + // Error message in text + m: String, + // range of the formula (width, height) + r: (i32, i32), + // true if the formula is a CSE formula + a: bool, + }, } impl Default for Cell { diff --git a/base/src/user_model/common.rs b/base/src/user_model/common.rs index 727da3a..47c9bec 100644 --- a/base/src/user_model/common.rs +++ b/base/src/user_model/common.rs @@ -24,6 +24,18 @@ use crate::user_model::history::{ }; use super::border_utils::is_max_border; + +#[derive(Serialize, Deserialize)] +pub enum CellArrayStructure { + // It s just a single cell + SingleCell, + // It is part o a dynamic array + // (mother_row, mother_column, width, height) + DynamicChild(i32, i32, i32, i32), + // Mother of a dynamic array (width, height) + DynamicMother(i32, i32), +} + /// Data for the clipboard pub type ClipboardData = HashMap>; @@ -627,6 +639,7 @@ impl UserModel { } } self.push_diff_list(diff_list); + self.evaluate_if_not_paused(); Ok(()) } @@ -656,6 +669,7 @@ impl UserModel { } } self.push_diff_list(diff_list); + self.evaluate_if_not_paused(); Ok(()) } @@ -1741,6 +1755,65 @@ impl UserModel { Ok(None) } + /// Returns the geometric structure of a cell + pub fn get_cell_array_structure( + &self, + sheet: u32, + row: i32, + column: i32, + ) -> Result { + let cell = self + .model + .workbook + .worksheet(sheet)? + .cell(row, column) + .cloned() + .unwrap_or_default(); + match cell { + Cell::EmptyCell { .. } + | Cell::BooleanCell { .. } + | Cell::NumberCell { .. } + | Cell::ErrorCell { .. } + | Cell::SharedString { .. } + | Cell::CellFormula { .. } + | Cell::CellFormulaBoolean { .. } + | Cell::CellFormulaNumber { .. } + | Cell::CellFormulaString { .. } + | Cell::CellFormulaError { .. } => Ok(CellArrayStructure::SingleCell), + Cell::SpillNumberCell { m, .. } + | Cell::SpillBooleanCell { m, .. } + | Cell::SpillErrorCell { m, .. } + | Cell::SpillStringCell { m, .. } => { + let (m_row, m_column) = m; + let m_cell = self + .model + .workbook + .worksheet(sheet)? + .cell(m_row, m_column) + .cloned() + .unwrap_or_default(); + let (width, height) = match m_cell { + Cell::DynamicCellFormula { r, .. } + | Cell::DynamicCellFormulaBoolean { r, .. } + | Cell::DynamicCellFormulaNumber { r, .. } + | Cell::DynamicCellFormulaString { r, .. } + | Cell::DynamicCellFormulaError { r, .. } => (r.0, r.1), + _ => return Err("Invalid structure".to_string()), + }; + Ok(CellArrayStructure::DynamicChild( + m_row, m_column, width, height, + )) + } + Cell::DynamicCellFormula { r, .. } + | Cell::DynamicCellFormulaBoolean { r, .. } + | Cell::DynamicCellFormulaNumber { r, .. } + | Cell::DynamicCellFormulaString { r, .. } + | Cell::DynamicCellFormulaError { r, .. } => { + Ok(CellArrayStructure::DynamicMother(r.0, r.1)) + } + } + } + /// Returns a copy of the selected area pub fn copy_to_clipboard(&self) -> Result { let selected_area = self.get_selected_view(); @@ -2043,6 +2116,24 @@ impl UserModel { old_value, } => { needs_evaluation = true; + let cell = self + .model + .workbook + .worksheet(*sheet)? + .cell(*row, *column) + .cloned() + .unwrap_or_default(); + if let Some((width, height)) = cell.get_dynamic_range() { + for r in *row..*row + height { + for c in *column..*column + width { + // skip the "mother" cell + if r == *row && c == *column { + continue; + } + self.model.cell_clear_contents(*sheet, r, c)?; + } + } + } match *old_value.clone() { Some(value) => { self.model diff --git a/base/src/user_model/history.rs b/base/src/user_model/history.rs index 9d45986..2d3d2fd 100644 --- a/base/src/user_model/history.rs +++ b/base/src/user_model/history.rs @@ -5,18 +5,21 @@ use bitcode::{Decode, Encode}; use crate::types::{Cell, Col, Row, SheetState, Style, Worksheet}; #[derive(Clone, Encode, Decode)] +#[cfg_attr(debug_assertions, derive(Debug))] pub(crate) struct RowData { pub(crate) row: Option, pub(crate) data: HashMap, } #[derive(Clone, Encode, Decode)] +#[cfg_attr(debug_assertions, derive(Debug))] pub(crate) struct ColumnData { pub(crate) column: Option, pub(crate) data: HashMap, } #[derive(Clone, Encode, Decode)] +#[cfg_attr(debug_assertions, derive(Debug))] pub(crate) enum Diff { // Cell diffs SetCellValue { diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 8517b0a..c46ae44 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -766,4 +766,18 @@ impl Model { .get_first_non_empty_in_row_after_column(sheet, row, column) .map_err(to_js_error) } + + #[wasm_bindgen(js_name = "getCellArrayStructure")] + pub fn get_cell_array_structure( + &self, + sheet: u32, + row: i32, + column: i32, + ) -> Result { + let cell_structure = self + .model + .get_cell_array_structure(sheet, row, column) + .map_err(|e| to_js_error(e.to_string()))?; + serde_wasm_bindgen::to_value(&cell_structure).map_err(JsError::from) + } } diff --git a/bindings/wasm/types.ts b/bindings/wasm/types.ts index 7af55b8..fcaa2ff 100644 --- a/bindings/wasm/types.ts +++ b/bindings/wasm/types.ts @@ -109,6 +109,11 @@ export interface MarkedToken { end: number; } +export type CellArrayStructure = + | "SingleCell" + | { DynamicChild: [number, number, number, number] } + | { DynamicMother: [number, number] }; + export interface WorksheetProperties { name: string; color: string; @@ -216,7 +221,7 @@ export interface SelectedView { // }; // type ClipboardData = Record>; -type ClipboardData = Map>; +type ClipboardData = Map>; export interface ClipboardCell { text: string; @@ -233,4 +238,4 @@ export interface DefinedName { name: string; scope?: number; formula: string; -} \ No newline at end of file +} diff --git a/webapp/IronCalc/src/components/FormulaBar/FormulaBar.tsx b/webapp/IronCalc/src/components/FormulaBar/FormulaBar.tsx index df1bfbb..5c52add 100644 --- a/webapp/IronCalc/src/components/FormulaBar/FormulaBar.tsx +++ b/webapp/IronCalc/src/components/FormulaBar/FormulaBar.tsx @@ -13,6 +13,7 @@ import type { WorkbookState } from "../workbookState"; type FormulaBarProps = { cellAddress: string; formulaValue: string; + isPartOfArray: boolean; model: Model; workbookState: WorkbookState; onChange: () => void; @@ -23,6 +24,7 @@ function FormulaBar(properties: FormulaBarProps) { const { cellAddress, formulaValue, + isPartOfArray, model, onChange, onTextUpdated, @@ -62,6 +64,9 @@ function FormulaBar(properties: FormulaBarProps) { event.stopPropagation(); event.preventDefault(); }} + sx={{ + color: isPartOfArray ? "grey" : "black", + }} > { return model.getCellContent(sheet, row, column); }; + // returns true if it is either single cell or the root cell of an array + const isRootCellOfArray = () => { + const { sheet, row, column } = model.getSelectedView(); + const r = model.getCellArrayStructure(sheet, row, column); + if (r === "SingleCell") { + return false; + } + if ("DynamicMother" in r) { + return false; + } + return true; + }; + const getCellStyle = useCallback(() => { const { sheet, row, column } = model.getSelectedView(); return model.getCellStyle(sheet, row, column); @@ -705,6 +718,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { }} model={model} workbookState={workbookState} + isPartOfArray={isRootCellOfArray()} /> (null); const cellOutline = useRef(null); const areaOutline = useRef(null); + const cellOutlineHandle = useRef(null); + const cellArrayStructure = useRef(null); const extendToOutline = useRef(null); const columnResizeGuide = useRef(null); const rowResizeGuide = useRef(null); @@ -85,6 +88,7 @@ const Worksheet = forwardRef( const outline = cellOutline.current; const area = areaOutline.current; + const arrayStructure = cellArrayStructure.current; const extendTo = extendToOutline.current; const editor = editorElement.current; @@ -98,7 +102,8 @@ const Worksheet = forwardRef( !area || !extendTo || !scrollElement.current || - !editor + !editor || + !arrayStructure ) return; // FIXME: This two need to be computed. @@ -115,6 +120,8 @@ const Worksheet = forwardRef( rowGuide: rowGuideRef, columnHeaders: columnHeadersRef, cellOutline: outline, + cellOutlineHandle: handle, + cellArrayStructure: arrayStructure, areaOutline: area, extendToOutline: extendTo, editor: editor, @@ -329,6 +336,7 @@ const Worksheet = forwardRef( /> + @@ -514,6 +522,12 @@ const AreaOutline = styled("div")` background-color: ${outlineBackgroundColor}; `; +const CellArrayStructure = styled("div")` + position: absolute; + border: 1px solid ${cellArrayStructureColor}; + border-radius: 3px; +`; + const CellOutline = styled("div")` position: absolute; border: 2px solid ${outlineColor}; diff --git a/webapp/IronCalc/src/components/WorksheetCanvas/constants.ts b/webapp/IronCalc/src/components/WorksheetCanvas/constants.ts index 2e657a5..01ffc52 100644 --- a/webapp/IronCalc/src/components/WorksheetCanvas/constants.ts +++ b/webapp/IronCalc/src/components/WorksheetCanvas/constants.ts @@ -13,6 +13,7 @@ export const defaultTextColor = "#2E414D"; export const outlineColor = "#F2994A"; export const outlineBackgroundColor = "#F2994A1A"; +export const cellArrayStructureColor = "#64BDFDA1"; export const LAST_COLUMN = 16_384; export const LAST_ROW = 1_048_576; diff --git a/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts b/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts index 82b1f7e..d2bde37 100644 --- a/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts +++ b/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts @@ -31,6 +31,8 @@ export interface CanvasSettings { canvas: HTMLCanvasElement; cellOutline: HTMLDivElement; areaOutline: HTMLDivElement; + cellOutlineHandle: HTMLDivElement; + cellArrayStructure: HTMLDivElement; extendToOutline: HTMLDivElement; columnGuide: HTMLDivElement; rowGuide: HTMLDivElement; @@ -90,6 +92,8 @@ export default class WorksheetCanvas { cellOutlineHandle: HTMLDivElement; + cellArrayStructure: HTMLDivElement; + extendToOutline: HTMLDivElement; workbookState: WorkbookState; @@ -124,6 +128,8 @@ export default class WorksheetCanvas { this.refresh = options.refresh; this.cellOutline = options.elements.cellOutline; + this.cellOutlineHandle = options.elements.cellOutlineHandle; + this.cellArrayStructure = options.elements.cellArrayStructure; this.areaOutline = options.elements.areaOutline; this.extendToOutline = options.elements.extendToOutline; this.rowGuide = options.elements.rowGuide; @@ -1515,16 +1521,20 @@ export default class WorksheetCanvas { } private drawCellOutline(): void { - const { cellOutline, areaOutline, cellOutlineHandle } = this; + const { cellArrayStructure, cellOutline, areaOutline, cellOutlineHandle } = + this; if (this.workbookState.getEditingCell()) { cellOutline.style.visibility = "hidden"; cellOutlineHandle.style.visibility = "hidden"; areaOutline.style.visibility = "hidden"; + cellArrayStructure.style.visibility = "hidden"; return; } cellOutline.style.visibility = "visible"; cellOutlineHandle.style.visibility = "visible"; areaOutline.style.visibility = "visible"; + // This one is hidden by default + cellArrayStructure.style.visibility = "hidden"; const [selectedSheet, selectedRow, selectedColumn] = this.model.getSelectedCell(); @@ -1580,6 +1590,34 @@ export default class WorksheetCanvas { [handleX, handleY] = this.getCoordinatesByCell(rowStart, columnStart); handleX += this.getColumnWidth(selectedSheet, columnStart); handleY += this.getRowHeight(selectedSheet, rowStart); + // we draw the array structure if needed only in this case + const arrayStructure = this.model.getCellArrayStructure( + selectedSheet, + selectedRow, + selectedColumn, + ); + let array = null; + if (arrayStructure === "SingleCell") { + // nothing to see here + } else if ("DynamicMother" in arrayStructure) { + cellArrayStructure.style.visibility = "visible"; + const [arrayWidth, arrayHeight] = arrayStructure.DynamicMother; + array = [selectedRow, selectedColumn, arrayWidth, arrayHeight]; + } else { + cellArrayStructure.style.visibility = "visible"; + array = arrayStructure.DynamicChild; + } + if (array !== null) { + const [arrayX, arrayY] = this.getCoordinatesByCell(array[0], array[1]); + const [arrayX1, arrayY1] = this.getCoordinatesByCell( + array[0] + array[3], + array[1] + array[2], + ); + cellArrayStructure.style.left = `${arrayX}px`; + cellArrayStructure.style.top = `${arrayY}px`; + cellArrayStructure.style.width = `${arrayX1 - arrayX}px`; + cellArrayStructure.style.height = `${arrayY1 - arrayY}px`; + } } else { areaOutline.style.visibility = "visible"; cellOutlineHandle.style.visibility = "visible"; diff --git a/xlsx/src/export/worksheets.rs b/xlsx/src/export/worksheets.rs index 62b844b..3186ccd 100644 --- a/xlsx/src/export/worksheets.rs +++ b/xlsx/src/export/worksheets.rs @@ -22,7 +22,7 @@ use itertools::Itertools; use ironcalc_base::{ expressions::{ - parser::{stringify::to_excel_string, Node}, + parser::{static_analysis::StaticResult, stringify::to_excel_string, Node}, types::CellReferenceRC, utils::number_to_column, }, @@ -56,7 +56,7 @@ fn get_formula_attribute( pub(crate) fn get_worksheet_xml( worksheet: &Worksheet, - parsed_formulas: &[Node], + parsed_formulas: &[(Node, StaticResult)], dimension: &str, is_sheet_selected: bool, ) -> String { @@ -104,7 +104,7 @@ pub(crate) fn get_worksheet_xml( let style = get_cell_style_attribute(*s); row_data_str.push(format!("")); } - Cell::BooleanCell { v, s } => { + Cell::SpillBooleanCell { v, s, .. } | Cell::BooleanCell { v, s } => { // // 1 // @@ -114,7 +114,7 @@ pub(crate) fn get_worksheet_xml( "{b}" )); } - Cell::NumberCell { v, s } => { + Cell::SpillNumberCell { v, s, .. } | Cell::NumberCell { v, s } => { // Normally the type number is left out. Example: // // 3 @@ -122,7 +122,7 @@ pub(crate) fn get_worksheet_xml( let style = get_cell_style_attribute(*s); row_data_str.push(format!("{v}")); } - Cell::ErrorCell { ei, s } => { + Cell::SpillErrorCell { ei, s, .. } | Cell::ErrorCell { ei, s } => { let style = get_cell_style_attribute(*s); row_data_str.push(format!( "{ei}" @@ -153,7 +153,7 @@ pub(crate) fn get_worksheet_xml( worksheet.get_name(), *row_index, *column_index, - &parsed_formulas[*f as usize], + &parsed_formulas[*f as usize].0, ); let b = i32::from(*v); @@ -172,7 +172,7 @@ pub(crate) fn get_worksheet_xml( worksheet.get_name(), *row_index, *column_index, - &parsed_formulas[*f as usize], + &parsed_formulas[*f as usize].0, ); let style = get_cell_style_attribute(*s); @@ -189,14 +189,14 @@ pub(crate) fn get_worksheet_xml( worksheet.get_name(), *row_index, *column_index, - &parsed_formulas[*f as usize], + &parsed_formulas[*f as usize].0, ); let style = get_cell_style_attribute(*s); let escaped_v = escape_xml(v); row_data_str.push(format!( - "{formula}{escaped_v}" - )); + "{formula}{escaped_v}" + )); } Cell::CellFormulaError { f, @@ -213,13 +213,135 @@ pub(crate) fn get_worksheet_xml( worksheet.get_name(), *row_index, *column_index, - &parsed_formulas[*f as usize], + &parsed_formulas[*f as usize].0, ); let style = get_cell_style_attribute(*s); row_data_str.push(format!( "{formula}{ei}" )); } + Cell::SpillStringCell { v, s, .. } => { + // inline string + // + let style = get_cell_style_attribute(*s); + let escaped_v = escape_xml(v); + row_data_str.push(format!( + "{escaped_v}" + )); + } + Cell::DynamicCellFormula { .. } => { + panic!("Model needs to be evaluated before saving!"); + } + Cell::DynamicCellFormulaBoolean { f, v, s, r, a: _ } => { + // + // A1:A10 + // 1 + // + let style = get_cell_style_attribute(*s); + let range = format!( + "{}{}:{}{}", + column_name, + row_index, + number_to_column(r.0 + column_index).unwrap(), + r.1 + row_index + ); + + let formula = get_formula_attribute( + worksheet.get_name(), + *row_index, + *column_index, + &parsed_formulas[*f as usize].0, + ); + + let b = i32::from(*v); + row_data_str.push(format!( + r#"{formula}{b}"# + )); + } + Cell::DynamicCellFormulaNumber { f, v, s, r, a: _ } => { + // + // C4:C10 + // 123 + // + let style = get_cell_style_attribute(*s); + let range = format!( + "{}{}:{}{}", + column_name, + row_index, + number_to_column(r.0 + column_index).unwrap(), + r.1 + row_index + ); + + let formula = get_formula_attribute( + worksheet.get_name(), + *row_index, + *column_index, + &parsed_formulas[*f as usize].0, + ); + + row_data_str.push(format!( + r#"{formula}{v}"# + )); + } + Cell::DynamicCellFormulaString { f, v, s, r, a: _ } => { + // + // C6:C10 + // Hello world! + // + let style = get_cell_style_attribute(*s); + let range = format!( + "{}{}:{}{}", + column_name, + row_index, + number_to_column(r.0 + column_index).unwrap(), + r.1 + row_index + ); + + let formula = get_formula_attribute( + worksheet.get_name(), + *row_index, + *column_index, + &parsed_formulas[*f as usize].0, + ); + let escaped_v = escape_xml(v); + + row_data_str.push(format!( + r#"{formula}{escaped_v}"# + )); + } + Cell::DynamicCellFormulaError { + f, + ei, + s, + o: _, + m: _, + r, + a: _, + } => { + // + // C6:C10 + // #DIV/0! + // + let style = get_cell_style_attribute(*s); + let range = format!( + "{}{}:{}{}", + column_name, + row_index, + number_to_column(r.0 + column_index).unwrap(), + r.1 + row_index + ); + + let formula = get_formula_attribute( + worksheet.get_name(), + *row_index, + *column_index, + &parsed_formulas[*f as usize].0, + ); + + row_data_str.push(format!( + r#"{formula}{ei}"# + )); + } } } let row_style_str = match row_style_dict.get(row_index) { diff --git a/xlsx/src/import/worksheets.rs b/xlsx/src/import/worksheets.rs index fc60783..858380b 100644 --- a/xlsx/src/import/worksheets.rs +++ b/xlsx/src/import/worksheets.rs @@ -303,13 +303,15 @@ fn from_a1_to_rc( context: String, tables: HashMap, defined_names: Vec, + is_array: bool, ) -> Result { 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 mut t = parser.parse(&formula, &cell_reference); - add_implicit_intersection(&mut t, true); - + if !is_array { + add_implicit_intersection(&mut t, true); + } Ok(to_rc_format(&t)) } @@ -837,6 +839,7 @@ pub(super) fn load_sheet( }; let cell_metadata = cell.attribute("cm"); + let is_dynamic_array = cell_metadata == Some("1"); // type, the default type being "n" for number // If the cell does not have a value is an empty cell @@ -903,6 +906,7 @@ pub(super) fn load_sheet( context, tables.clone(), defined_names.clone(), + is_dynamic_array, )?; match index_map.get(&si) { Some(index) => { @@ -951,7 +955,6 @@ pub(super) fn load_sheet( return Err(XlsxError::NotImplemented("data table formulas".to_string())); } "array" | "normal" => { - let is_dynamic_array = cell_metadata == Some("1"); if formula_type == "array" && !is_dynamic_array { // Dynamic formulas in Excel are formulas of type array with the cm=1, those we support. // On the other hand the old CSE formulas or array formulas are not supported in IronCalc for the time being @@ -966,6 +969,7 @@ pub(super) fn load_sheet( context, tables.clone(), defined_names.clone(), + is_dynamic_array, )?; match get_formula_index(&formula, &shared_formulas) { diff --git a/xlsx/tests/calc_tests/simple_spill.xlsx b/xlsx/tests/calc_tests/simple_spill.xlsx new file mode 100644 index 0000000..cf38cf6 Binary files /dev/null and b/xlsx/tests/calc_tests/simple_spill.xlsx differ