From d445553d8566fd073a0fb671017d90bc52dd9b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher=20Andr=C3=A9s?= Date: Wed, 3 Apr 2024 22:41:15 +0200 Subject: [PATCH] UPDATE: Adds 'user model' API (#27) * bump version for documentation * Fixes wrong doc comment * renames old APIs to be consistent --- Cargo.lock | 2 +- base/Cargo.toml | 2 +- base/examples/formulas_and_errors.rs | 2 +- base/examples/hello_world.rs | 2 +- base/src/actions.rs | 75 +- base/src/functions/information.rs | 3 +- base/src/lib.rs | 16 +- base/src/model.rs | 232 ++-- base/src/new_empty.rs | 13 +- base/src/test/mod.rs | 5 +- base/src/test/test_actions.rs | 245 ++++ ...l_empty.rs => test_cell_clear_contents.rs} | 16 +- base/src/test/test_column_width.rs | 20 +- base/src/test/test_frozen_rows_and_columns.rs | 23 +- base/src/test/test_general.rs | 28 +- ...e_cell.rs => test_model_cell_clear_all.rs} | 16 +- base/src/test/test_model_is_empty_cell.rs | 2 +- base/src/test/test_sheet_markup.rs | 2 +- base/src/test/test_sheets.rs | 8 + base/src/test/test_workbook.rs | 2 +- base/src/test/test_worksheet.rs | 6 +- base/src/test/user_model/mod.rs | 9 + .../test/user_model/test_add_delete_sheets.rs | 58 + base/src/test/user_model/test_clear_cells.rs | 91 ++ base/src/test/user_model/test_diff_queue.rs | 159 +++ base/src/test/user_model/test_evaluation.rs | 31 + base/src/test/user_model/test_general.rs | 103 ++ base/src/test/user_model/test_rename_sheet.rs | 39 + base/src/test/user_model/test_row_column.rs | 156 +++ base/src/test/user_model/test_styles.rs | 711 ++++++++++ base/src/test/user_model/test_undo_redo.rs | 66 + base/src/test/util.rs | 4 +- base/src/user_model.rs | 1183 +++++++++++++++++ base/src/workbook.rs | 12 - base/src/worksheet.rs | 100 +- xlsx/examples/hello_calc.rs | 2 +- xlsx/examples/hello_styles.rs | 2 +- xlsx/examples/widths_and_heights.rs | 4 +- xlsx/src/bin/documentation.rs | 2 +- xlsx/src/compare.rs | 4 +- xlsx/src/export/mod.rs | 2 +- xlsx/src/export/test/test_export.rs | 33 +- xlsx/src/import/mod.rs | 2 +- xlsx/src/lib.rs | 4 +- xlsx/tests/test.rs | 4 +- 45 files changed, 3233 insertions(+), 268 deletions(-) rename base/src/test/{test_model_set_cell_empty.rs => test_cell_clear_contents.rs} (77%) rename base/src/test/{test_model_delete_cell.rs => test_model_cell_clear_all.rs} (76%) create mode 100644 base/src/test/user_model/mod.rs create mode 100644 base/src/test/user_model/test_add_delete_sheets.rs create mode 100644 base/src/test/user_model/test_clear_cells.rs create mode 100644 base/src/test/user_model/test_diff_queue.rs create mode 100644 base/src/test/user_model/test_evaluation.rs create mode 100644 base/src/test/user_model/test_general.rs create mode 100644 base/src/test/user_model/test_rename_sheet.rs create mode 100644 base/src/test/user_model/test_row_column.rs create mode 100644 base/src/test/user_model/test_styles.rs create mode 100644 base/src/test/user_model/test_undo_redo.rs create mode 100644 base/src/user_model.rs diff --git a/Cargo.lock b/Cargo.lock index 850add1..dc9c6ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,7 +204,7 @@ dependencies = [ [[package]] name = "ironcalc_base" -version = "0.1.2" +version = "0.1.3" dependencies = [ "chrono", "chrono-tz", diff --git a/base/Cargo.toml b/base/Cargo.toml index 27c11c4..7884e3a 100644 --- a/base/Cargo.toml +++ b/base/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironcalc_base" -version = "0.1.2" +version = "0.1.3" authors = ["Nicolás Hatcher "] edition = "2021" homepage = "https://www.ironcalc.com" diff --git a/base/examples/formulas_and_errors.rs b/base/examples/formulas_and_errors.rs index 8333a88..b279c17 100644 --- a/base/examples/formulas_and_errors.rs +++ b/base/examples/formulas_and_errors.rs @@ -1,4 +1,4 @@ -use ironcalc_base::{model::Model, types::CellType}; +use ironcalc_base::{types::CellType, Model}; fn main() -> Result<(), Box> { let mut model = Model::new_empty("formulas-and-errors", "en", "UTC")?; diff --git a/base/examples/hello_world.rs b/base/examples/hello_world.rs index 21d2881..4275421 100644 --- a/base/examples/hello_world.rs +++ b/base/examples/hello_world.rs @@ -1,4 +1,4 @@ -use ironcalc_base::{cell::CellValue, model::Model}; +use ironcalc_base::{cell::CellValue, Model}; fn main() -> Result<(), Box> { let mut model = Model::new_empty("hello-world", "en", "UTC")?; diff --git a/base/src/actions.rs b/base/src/actions.rs index bdea119..bd79520 100644 --- a/base/src/actions.rs +++ b/base/src/actions.rs @@ -77,13 +77,13 @@ impl Model { let style = source_cell.get_style(); // FIXME: we need some user_input getter instead of get_text let formula_or_value = self - .cell_formula(sheet, source_row, source_column)? + .get_cell_formula(sheet, source_row, source_column)? .unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language)); self.set_user_input(sheet, target_row, target_column, formula_or_value); self.workbook .worksheet_mut(sheet)? .set_cell_style(target_row, target_column, style); - self.delete_cell(sheet, source_row, source_column)?; + self.cell_clear_all(sheet, source_row, source_column)?; Ok(()) } @@ -157,6 +157,11 @@ impl Model { return Err("Please use insert columns instead".to_string()); } + // first column being deleted + let column_start = column; + // last column being deleted + let column_end = column + column_count - 1; + // Move cells let worksheet = &self.workbook.worksheet(sheet)?; let mut all_rows: Vec = worksheet.sheet_data.keys().copied().collect(); @@ -166,11 +171,11 @@ impl Model { for r in all_rows { let columns: Vec = self.get_columns_for_row(sheet, r, false)?; for col in columns { - if col >= column { - if col >= column + column_count { + if col >= column_start { + if col > column_end { self.move_cell(sheet, r, col, r, col - column_count)?; } else { - self.delete_cell(sheet, r, col)?; + self.cell_clear_all(sheet, r, col)?; } } } @@ -184,6 +189,64 @@ impl Model { delta: -column_count, }), ); + let worksheet = &mut self.workbook.worksheet_mut(sheet)?; + + // deletes all the column styles + let mut new_columns = Vec::new(); + for col in worksheet.cols.iter_mut() { + // range under study + let min = col.min; + let max = col.max; + // In the diagram: + // |xxxxx| range we are studying [min, max] + // |*****| range we are deleting [column_start, column_end] + // we are going to split it in three big cases: + // ----------------|xxxxxxxx|----------------- + // -----|*****|------------------------------- Case A + // -------|**********|------------------------ Case B + // -------------|**************|-------------- Case C + // ------------------|****|------------------- Case D + // ---------------------|**********|---------- Case E + // -----------------------------|*****|------- Case F + if column_start < min { + if column_end < min { + // Case A + // We displace all columns + let mut new_column = col.clone(); + new_column.min = min - column_count; + new_column.max = max - column_count; + new_columns.push(new_column); + } else if column_end < max { + // Case B + // We displace the end + let mut new_column = col.clone(); + new_column.min = column_start; + new_column.max = max - column_count; + new_columns.push(new_column); + } else { + // Case C + // skip this, we are deleting the whole range + } + } else if column_start <= max { + if column_end <= max { + // Case D + // We displace the end + let mut new_column = col.clone(); + new_column.max = max - column_count; + new_columns.push(new_column); + } else { + // Case E + let mut new_column = col.clone(); + new_column.max = column_start - 1; + new_columns.push(new_column); + } + } else { + // Case F + // No action required + new_columns.push(col.clone()); + } + } + worksheet.cols = new_columns; Ok(()) } @@ -283,7 +346,7 @@ impl Model { // remove all cells in row // FIXME: We could just remove the entire row in one go for column in columns { - self.delete_cell(sheet, r, column)?; + self.cell_clear_all(sheet, r, column)?; } } } diff --git a/base/src/functions/information.rs b/base/src/functions/information.rs index 18f11fb..6d2aaa7 100644 --- a/base/src/functions/information.rs +++ b/base/src/functions/information.rs @@ -165,7 +165,8 @@ impl Model { message: "argument must be a reference to a single cell".to_string(), }; } - let is_formula = if let Ok(f) = self.cell_formula(left.sheet, left.row, left.column) { + let is_formula = if let Ok(f) = self.get_cell_formula(left.sheet, left.row, left.column) + { f.is_some() } else { false diff --git a/base/src/lib.rs b/base/src/lib.rs index e16f282..a5c632b 100644 --- a/base/src/lib.rs +++ b/base/src/lib.rs @@ -8,7 +8,7 @@ //! //! ```toml //! [dependencies] -//! ironcalc_base = { git = "https://github.com/ironcalc/IronCalc", version = "0.1"} +//! ironcalc_base = { git = "https://github.com/ironcalc/IronCalc" } //! ``` //! //! until version 0.5.0 you should use the git dependencies as stated @@ -31,23 +31,21 @@ pub mod expressions; pub mod formatter; pub mod language; pub mod locale; -pub mod model; pub mod new_empty; pub mod number_format; pub mod types; pub mod worksheet; -mod functions; - mod actions; mod cast; mod constants; -mod styles; - mod diffs; +mod functions; mod implicit_intersection; - +mod model; +mod styles; mod units; +mod user_model; mod utils; mod workbook; @@ -56,3 +54,7 @@ mod test; #[cfg(test)] pub mod mock_time; + +pub use model::get_milliseconds_since_epoch; +pub use model::Model; +pub use user_model::UserModel; diff --git a/base/src/model.rs b/base/src/model.rs index c04872c..0bf7f29 100644 --- a/base/src/model.rs +++ b/base/src/model.rs @@ -1,12 +1,5 @@ #![deny(missing_docs)] -//! # Model -//! -//! Note that sheets are 0-indexed and rows and columns are 1-indexed. -//! -//! IronCalc is row first. A cell is referenced by (`sheet`, `row`, `column`) -//! - use serde_json::json; use std::collections::HashMap; @@ -47,7 +40,11 @@ use chrono_tz::Tz; #[cfg(test)] pub use crate::mock_time::get_milliseconds_since_epoch; -/// wasm implementation for time +/// Number of milliseconds since January 1, 1970 +/// Used by time and date functions. It takes the value from the environment: +/// * The Operative System +/// * The JavaScript environment +/// * Or mocked for tests #[cfg(not(test))] #[cfg(not(target_arch = "wasm32"))] pub fn get_milliseconds_since_epoch() -> i64 { @@ -67,7 +64,7 @@ pub fn get_milliseconds_since_epoch() -> i64 { /// A cell might be evaluated or being evaluated #[derive(Clone)] -pub enum CellState { +pub(crate) enum CellState { /// The cell has already been evaluated Evaluated, /// The cell is being evaluated @@ -75,7 +72,7 @@ pub enum CellState { } /// A parsed formula for a defined name -pub enum ParsedDefinedName { +pub(crate) enum ParsedDefinedName { /// CellReference (`=C4`) CellReference(CellReferenceIndex), /// A Range (`=C4:D6`) @@ -105,19 +102,19 @@ pub struct Model { /// A list of parsed formulas pub parsed_formulas: Vec>, /// A list of parsed defined names - pub parsed_defined_names: HashMap<(Option, String), ParsedDefinedName>, + pub(crate) parsed_defined_names: HashMap<(Option, String), ParsedDefinedName>, /// An optimization to lookup strings faster - pub shared_strings: HashMap, + pub(crate) shared_strings: HashMap, /// An instance of the parser - pub parser: Parser, + pub(crate) parser: Parser, /// The list of cells with formulas that are evaluated of being evaluated - pub cells: HashMap<(u32, i32, i32), CellState>, + pub(crate) cells: HashMap<(u32, i32, i32), CellState>, /// The locale of the model - pub locale: Locale, + pub(crate) locale: Locale, /// Tha language used - pub language: Language, + pub(crate) language: Language, /// The timezone used to evaluate the model - pub tz: Tz, + pub(crate) tz: Tz, } // FIXME: Maybe this should be the same as CellReference @@ -659,7 +656,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; /// assert_eq!(model.workbook.worksheet(0)?.color, None); @@ -726,7 +723,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; /// assert_eq!(model.is_empty_cell(0, 1, 1)?, true); @@ -803,7 +800,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # use ironcalc_base::cell::CellValue; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; @@ -827,7 +824,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # use ironcalc_base::cell::CellValue; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; @@ -896,7 +893,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # use ironcalc_base::expressions::types::CellReferenceIndex; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; @@ -965,7 +962,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # use ironcalc_base::expressions::types::{Area, CellReferenceIndex}; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; @@ -1036,7 +1033,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; /// let (sheet, row, column) = (0, 1, 1); @@ -1083,7 +1080,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # use ironcalc_base::expressions::types::CellReferenceIndex; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; @@ -1138,13 +1135,13 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; /// let (sheet, row, column) = (0, 1, 1); /// model.set_user_input(sheet, row, column, "=SIN(B1*C3)+1".to_string()); /// model.evaluate(); - /// let result = model.cell_formula(sheet, row, column)?; + /// let result = model.get_cell_formula(sheet, row, column)?; /// assert_eq!(result, Some("=SIN(B1*C3)+1".to_string())); /// # Ok(()) /// # } @@ -1152,24 +1149,33 @@ impl Model { /// /// See also: /// * [Model::get_cell_content()] - pub fn cell_formula( + pub fn get_cell_formula( &self, sheet: u32, row: i32, column: i32, ) -> Result, String> { let worksheet = self.workbook.worksheet(sheet)?; - Ok(worksheet.cell(row, column).and_then(|cell| { - cell.get_formula().map(|formula_index| { - let formula = &self.parsed_formulas[sheet as usize][formula_index as usize]; - let cell_ref = CellReferenceRC { - sheet: worksheet.get_name(), - row, - column, - }; - format!("={}", to_string(formula, &cell_ref)) - }) - })) + match worksheet.cell(row, column) { + Some(cell) => match cell.get_formula() { + Some(formula_index) => { + let formula = &self + .parsed_formulas + .get(sheet as usize) + .ok_or("missing sheet")? + .get(formula_index as usize) + .ok_or("missing formula")?; + let cell_ref = CellReferenceRC { + sheet: worksheet.get_name(), + row, + column, + }; + Ok(Some(format!("={}", to_string(formula, &cell_ref)))) + } + None => Ok(None), + }, + None => Ok(None), + } } /// Updates the value of a cell with some text @@ -1178,7 +1184,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; /// let (sheet, row, column) = (0, 1, 1); @@ -1221,7 +1227,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; /// let (sheet, row, column) = (0, 1, 1); @@ -1258,7 +1264,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; /// let (sheet, row, column) = (0, 1, 1); @@ -1296,7 +1302,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; /// let (sheet, row, column) = (0, 1, 1); @@ -1348,7 +1354,7 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # use ironcalc_base::cell::CellValue; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; @@ -1358,7 +1364,7 @@ impl Model { /// model.set_user_input(0, 1, 2, "=SUM(A:A)".to_string()); /// model.evaluate(); /// assert_eq!(model.get_cell_value_by_index(0, 1, 2), Ok(CellValue::Number(215.0))); - /// assert_eq!(model.formatted_cell_value(0, 1, 2), Ok("215$".to_string())); + /// assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("215$".to_string())); /// # Ok(()) /// # } /// ``` @@ -1529,7 +1535,7 @@ impl Model { /// Returns the cell value for (`sheet`, `row`, `column`) /// /// See also: - /// * [Model::formatted_cell_value()] + /// * [Model::get_formatted_cell_value()] pub fn get_cell_value_by_index( &self, sheet_index: u32, @@ -1555,35 +1561,34 @@ impl Model { /// # Examples /// /// ```rust - /// # use ironcalc_base::model::Model; + /// # use ironcalc_base::Model; /// # fn main() -> Result<(), Box> { /// let mut model = Model::new_empty("model", "en", "UTC")?; /// let (sheet, row, column) = (0, 1, 1); /// model.set_user_input(sheet, row, column, "=1/3".to_string()); /// model.evaluate(); - /// let result = model.formatted_cell_value(sheet, row, column)?; + /// let result = model.get_formatted_cell_value(sheet, row, column)?; /// assert_eq!(result, "0.333333333".to_string()); /// # Ok(()) /// # } /// ``` - pub fn formatted_cell_value( + pub fn get_formatted_cell_value( &self, sheet_index: u32, row: i32, column: i32, ) -> Result { - let format = self.get_style_for_cell(sheet_index, row, column).num_fmt; - let cell = self - .workbook - .worksheet(sheet_index)? - .cell(row, column) - .cloned() - .unwrap_or_default(); - let formatted_value = - cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| { - format_number(value, &format, &self.locale).text - }); - Ok(formatted_value) + match self.workbook.worksheet(sheet_index)?.cell(row, column) { + Some(cell) => { + let format = self.get_style_for_cell(sheet_index, row, column).num_fmt; + let formatted_value = + cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| { + format_number(value, &format, &self.locale).text + }); + Ok(formatted_value) + } + None => Ok("".to_string()), + } } /// Returns a string with the cell content. If there is a formula returns the formula @@ -1647,15 +1652,53 @@ impl Model { } } - /// Sets cell to empty. Can be used to delete value without affecting style. - pub fn set_cell_empty(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { - let worksheet = self.workbook.worksheet_mut(sheet)?; - worksheet.set_cell_empty(row, column); + /// Removes the content of the cell but leaves the style. + /// + /// See also: + /// * [Model::cell_clear_all()] + /// + /// # Examples + /// + /// ```rust + /// # use ironcalc_base::Model; + /// # fn main() -> Result<(), Box> { + /// let mut model = Model::new_empty("model", "en", "UTC")?; + /// let (sheet, row, column) = (0, 1, 1); + /// model.set_user_input(sheet, row, column, "100$".to_string()); + /// model.cell_clear_contents(sheet, row, column); + /// model.set_user_input(sheet, row, column, "10".to_string()); + /// let result = model.get_formatted_cell_value(sheet, row, column)?; + /// assert_eq!(result, "10$".to_string()); + /// # Ok(()) + /// # } + /// ``` + 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); Ok(()) } - /// Deletes a cell by removing it from worksheet data. - pub fn delete_cell(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { + /// Deletes a cell by removing it from worksheet data. All content and style is removed. + /// + /// See also: + /// * [Model::cell_clear_contents()] + /// + /// # Examples + /// + /// ```rust + /// # use ironcalc_base::Model; + /// # fn main() -> Result<(), Box> { + /// let mut model = Model::new_empty("model", "en", "UTC")?; + /// let (sheet, row, column) = (0, 1, 1); + /// model.set_user_input(sheet, row, column, "100$".to_string()); + /// model.cell_clear_all(sheet, row, column); + /// model.set_user_input(sheet, row, column, "10".to_string()); + /// let result = model.get_formatted_cell_value(sheet, row, column)?; + /// assert_eq!(result, "10".to_string()); + /// # Ok(()) + /// # } + pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { let worksheet = self.workbook.worksheet_mut(sheet)?; let sheet_data = &mut worksheet.sheet_data; @@ -1682,9 +1725,8 @@ impl Model { if r.r == row { if r.custom_format { return r.s; - } else { - break; } + break; } } let cols = &self.workbook.worksheets[sheet as usize].cols; @@ -1718,8 +1760,22 @@ impl Model { } } + /// Returns data about the worksheets + pub fn get_sheets_info(&self) -> Vec { + self.workbook + .worksheets + .iter() + .map(|worksheet| SheetInfo { + name: worksheet.get_name(), + state: worksheet.state.to_string(), + color: worksheet.color.clone(), + sheet_id: worksheet.sheet_id, + }) + .collect() + } + /// Returns markup representation of the given `sheet`. - pub fn sheet_markup(&self, sheet: u32) -> Result { + pub fn get_sheet_markup(&self, sheet: u32) -> Result { let worksheet = self.workbook.worksheet(sheet)?; let dimension = worksheet.dimension(); @@ -1729,9 +1785,9 @@ impl Model { let mut row_markup: Vec = Vec::new(); for column in 1..(dimension.max_column + 1) { - let mut cell_markup = match self.cell_formula(sheet, row, column)? { + let mut cell_markup = match self.get_cell_formula(sheet, row, column)? { Some(formula) => formula, - None => self.formatted_cell_value(sheet, row, column)?, + None => self.get_formatted_cell_value(sheet, row, column)?, }; let style = self.get_style_for_cell(sheet, row, column); if style.font.b { @@ -1770,7 +1826,7 @@ impl Model { } /// Returns the number of frozen rows in `sheet` - pub fn get_frozen_rows(&self, sheet: u32) -> Result { + pub fn get_frozen_rows_count(&self, sheet: u32) -> Result { if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) { Ok(worksheet.frozen_rows) } else { @@ -1779,7 +1835,7 @@ impl Model { } /// Return the number of frozen columns in `sheet` - pub fn get_frozen_columns(&self, sheet: u32) -> Result { + pub fn get_frozen_columns_count(&self, sheet: u32) -> Result { if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) { Ok(worksheet.frozen_columns) } else { @@ -1820,6 +1876,34 @@ impl Model { Err("Invalid sheet".to_string()) } } + + /// Returns the width of a column + #[inline] + pub fn get_column_width(&self, sheet: u32, column: i32) -> Result { + self.workbook.worksheet(sheet)?.get_column_width(column) + } + + /// Sets the width of a column + #[inline] + pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), String> { + self.workbook + .worksheet_mut(sheet)? + .set_column_width(column, width) + } + + /// Returns the height of a row + #[inline] + pub fn get_row_height(&self, sheet: u32, row: i32) -> Result { + self.workbook.worksheet(sheet)?.row_height(row) + } + + /// Sets the height of a row + #[inline] + pub fn set_row_height(&mut self, sheet: u32, column: i32, height: f64) -> Result<(), String> { + self.workbook + .worksheet_mut(sheet)? + .set_row_height(column, height) + } } #[cfg(test)] diff --git a/base/src/new_empty.rs b/base/src/new_empty.rs index 52cc0b2..81216fd 100644 --- a/base/src/new_empty.rs +++ b/base/src/new_empty.rs @@ -123,7 +123,7 @@ impl Model { } // Reparses all formulas and defined names - fn reset_parsed_structures(&mut self) { + pub(crate) fn reset_parsed_structures(&mut self) { self.parser .set_worksheets(self.workbook.get_worksheet_names()); self.parsed_formulas = vec![]; @@ -134,10 +134,10 @@ impl Model { } /// Adds a sheet with a automatically generated name - pub fn new_sheet(&mut self) { + pub fn new_sheet(&mut self) -> (String, u32) { // First we find a name - // TODO: When/if we support i18n the name could depend on the locale + // TODO: The name should depend on the locale let base_name = "Sheet"; let base_name_uppercase = base_name.to_uppercase(); let mut index = 1; @@ -156,6 +156,7 @@ impl Model { let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id); self.workbook.worksheets.push(worksheet); self.reset_parsed_structures(); + (sheet_name, self.workbook.worksheets.len() as u32 - 1) } /// Inserts a sheet with a particular index @@ -223,10 +224,10 @@ impl Model { new_name: &str, ) -> Result<(), String> { if !is_valid_sheet_name(new_name) { - return Err(format!("Invalid name for a sheet: '{}'", new_name)); + return Err(format!("Invalid name for a sheet: '{}'.", new_name)); } if self.get_sheet_index_by_name(new_name).is_some() { - return Err(format!("Sheet already exists: '{}'", new_name)); + return Err(format!("Sheet already exists: '{}'.", new_name)); } let worksheets = &self.workbook.worksheets; let sheet_count = worksheets.len() as u32; @@ -270,7 +271,7 @@ impl Model { if sheet_count == 1 { return Err("Cannot delete only sheet".to_string()); }; - if sheet_index > sheet_count { + if sheet_index >= sheet_count { return Err("Sheet index too large".to_string()); } self.workbook.worksheets.remove(sheet_index as usize); diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index a33aa35..a3ec5c4 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -1,6 +1,7 @@ mod test_actions; mod test_binary_search; mod test_cell; +mod test_cell_clear_contents; mod test_circular_references; mod test_column_width; mod test_criteria; @@ -28,9 +29,8 @@ mod test_frozen_rows_columns; mod test_general; mod test_math; mod test_metadata; -mod test_model_delete_cell; +mod test_model_cell_clear_all; mod test_model_is_empty_cell; -mod test_model_set_cell_empty; mod test_move_formula; mod test_quote_prefix; mod test_set_user_input; @@ -53,3 +53,4 @@ mod test_frozen_rows_and_columns; mod test_get_cell_content; mod test_percentage; mod test_today; +mod user_model; diff --git a/base/src/test/test_actions.rs b/base/src/test/test_actions.rs index b220132..45198e2 100644 --- a/base/src/test/test_actions.rs +++ b/base/src/test/test_actions.rs @@ -3,6 +3,7 @@ use crate::constants::LAST_COLUMN; use crate::model::Model; use crate::test::util::new_empty_model; +use crate::types::Col; #[test] fn test_insert_columns() { @@ -195,6 +196,250 @@ fn test_delete_columns() { assert_eq!(model._get_formula("A3"), *"=SUM(#REF!:K4)"); } +#[test] +fn test_delete_column_width() { + let mut model = new_empty_model(); + let (sheet, column) = (0, 5); + let normal_width = model.get_column_width(sheet, column).unwrap(); + // Set the width of one column to 5 times the normal width + assert!(model + .set_column_width(sheet, column, normal_width * 5.0) + .is_ok()); + + // delete it + assert!(model.delete_columns(sheet, column, 1).is_ok()); + + // all the columns around have the expected width + assert_eq!( + model.get_column_width(sheet, column - 1).unwrap(), + normal_width + ); + assert_eq!(model.get_column_width(sheet, column).unwrap(), normal_width); + assert_eq!( + model.get_column_width(sheet, column + 1).unwrap(), + normal_width + ); +} + +#[test] +// We set the style of columns 4 to 7 and delete column 4 +// We check that columns 4 to 6 have the new style +fn test_delete_first_column_width() { + let mut model = new_empty_model(); + model.workbook.worksheets[0].cols = vec![Col { + min: 4, + max: 7, + width: 300.0, + custom_width: true, + style: None, + }]; + let (sheet, column) = (0, 4); + assert!(model.delete_columns(sheet, column, 1).is_ok()); + let cols = &model.workbook.worksheets[0].cols; + assert_eq!(cols.len(), 1); + assert_eq!( + cols[0], + Col { + min: 4, + max: 6, + width: 300.0, + custom_width: true, + style: None + } + ); +} + +#[test] +// Delete the last column in the range +fn test_delete_last_column_width() { + let mut model = new_empty_model(); + model.workbook.worksheets[0].cols = vec![Col { + min: 4, + max: 7, + width: 300.0, + custom_width: true, + style: None, + }]; + let (sheet, column) = (0, 7); + assert!(model.delete_columns(sheet, column, 1).is_ok()); + let cols = &model.workbook.worksheets[0].cols; + assert_eq!(cols.len(), 1); + assert_eq!( + cols[0], + Col { + min: 4, + max: 6, + width: 300.0, + custom_width: true, + style: None + } + ); +} + +#[test] +// Deletes columns at the end +fn test_delete_last_few_columns_width() { + let mut model = new_empty_model(); + model.workbook.worksheets[0].cols = vec![Col { + min: 4, + max: 17, + width: 300.0, + custom_width: true, + style: None, + }]; + let (sheet, column) = (0, 13); + assert!(model.delete_columns(sheet, column, 10).is_ok()); + let cols = &model.workbook.worksheets[0].cols; + assert_eq!(cols.len(), 1); + assert_eq!( + cols[0], + Col { + min: 4, + max: 12, + width: 300.0, + custom_width: true, + style: None + } + ); +} + +#[test] +fn test_delete_columns_non_overlapping_left() { + let mut model = new_empty_model(); + model.workbook.worksheets[0].cols = vec![Col { + min: 10, + max: 17, + width: 300.0, + custom_width: true, + style: None, + }]; + let (sheet, column) = (0, 3); + assert!(model.delete_columns(sheet, column, 4).is_ok()); + let cols = &model.workbook.worksheets[0].cols; + assert_eq!(cols.len(), 1); + assert_eq!( + cols[0], + Col { + min: 6, + max: 13, + width: 300.0, + custom_width: true, + style: None + } + ); +} + +#[test] +fn test_delete_columns_overlapping_left() { + let mut model = new_empty_model(); + model.workbook.worksheets[0].cols = vec![Col { + min: 10, + max: 20, + width: 300.0, + custom_width: true, + style: None, + }]; + let (sheet, column) = (0, 8); + assert!(model.delete_columns(sheet, column, 4).is_ok()); + let cols = &model.workbook.worksheets[0].cols; + assert_eq!(cols.len(), 1); + assert_eq!( + cols[0], + Col { + min: 8, + max: 16, + width: 300.0, + custom_width: true, + style: None + } + ); +} + +#[test] +fn test_delete_columns_non_overlapping_right() { + let mut model = new_empty_model(); + model.workbook.worksheets[0].cols = vec![Col { + min: 10, + max: 17, + width: 300.0, + custom_width: true, + style: None, + }]; + let (sheet, column) = (0, 23); + assert!(model.delete_columns(sheet, column, 4).is_ok()); + let cols = &model.workbook.worksheets[0].cols; + assert_eq!(cols.len(), 1); + assert_eq!( + cols[0], + Col { + min: 10, + max: 17, + width: 300.0, + custom_width: true, + style: None + } + ); +} + +#[test] +// deletes some columns in the middle of the range +fn test_delete_middle_column_width() { + let mut model = new_empty_model(); + // styled columns [4, 17] + model.workbook.worksheets[0].cols = vec![Col { + min: 4, + max: 17, + width: 300.0, + custom_width: true, + style: None, + }]; + + // deletes columns 10, 11, 12 + let (sheet, column) = (0, 10); + assert!(model.delete_columns(sheet, column, 3).is_ok()); + let cols = &model.workbook.worksheets[0].cols; + assert_eq!(cols.len(), 1); + assert_eq!( + cols[0], + Col { + min: 4, + max: 14, + width: 300.0, + custom_width: true, + style: None + } + ); +} + +#[test] +// the range is inside the deleted columns +fn delete_range_in_columns() { + let mut model = new_empty_model(); + // styled columns [6, 10] + model.workbook.worksheets[0].cols = vec![Col { + min: 6, + max: 10, + width: 300.0, + custom_width: true, + style: None, + }]; + + // deletes columns [4, 17] + let (sheet, column) = (0, 4); + assert!(model.delete_columns(sheet, column, 8).is_ok()); + let cols = &model.workbook.worksheets[0].cols; + assert_eq!(cols.len(), 0); +} + +#[test] +fn test_delete_columns_error() { + let mut model = new_empty_model(); + let (sheet, column) = (0, 5); + assert!(model.delete_columns(sheet, column, -1).is_err()); + assert!(model.delete_columns(sheet, column, 0).is_err()); + assert!(model.delete_columns(sheet, column, 1).is_ok()); +} + #[test] fn test_delete_rows() { let mut model = new_empty_model(); diff --git a/base/src/test/test_model_set_cell_empty.rs b/base/src/test/test_cell_clear_contents.rs similarity index 77% rename from base/src/test/test_model_set_cell_empty.rs rename to base/src/test/test_cell_clear_contents.rs index decb0fb..1598a0b 100644 --- a/base/src/test/test_model_set_cell_empty.rs +++ b/base/src/test/test_cell_clear_contents.rs @@ -2,25 +2,25 @@ use crate::test::util::new_empty_model; #[test] -fn test_set_cell_empty_non_existing_sheet() { +fn test_cell_clear_contents_non_existing_sheet() { let mut model = new_empty_model(); assert_eq!( - model.set_cell_empty(13, 1, 1), + model.cell_clear_contents(13, 1, 1), Err("Invalid sheet index".to_string()) ); } #[test] -fn test_set_cell_empty_unset_cell() { +fn test_cell_clear_contents_unset_cell() { let mut model = new_empty_model(); - model.set_cell_empty(0, 1, 1).unwrap(); + model.cell_clear_contents(0, 1, 1).unwrap(); assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true)); model.evaluate(); assert_eq!(model._get_text_at(0, 1, 1), ""); } #[test] -fn test_set_cell_empty_with_value() { +fn test_cell_clear_contents_with_value() { let mut model = new_empty_model(); model._set("A1", "hello"); model.evaluate(); @@ -28,7 +28,7 @@ fn test_set_cell_empty_with_value() { assert_eq!(model._get_text_at(0, 1, 1), "hello"); assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); - model.set_cell_empty(0, 1, 1).unwrap(); + model.cell_clear_contents(0, 1, 1).unwrap(); model.evaluate(); assert_eq!(model._get_text_at(0, 1, 1), ""); @@ -36,7 +36,7 @@ fn test_set_cell_empty_with_value() { } #[test] -fn test_set_cell_empty_referenced_elsewhere() { +fn test_cell_clear_contents_referenced_elsewhere() { let mut model = new_empty_model(); model._set("A1", "35"); model._set("A2", "=2*A1"); @@ -47,7 +47,7 @@ fn test_set_cell_empty_referenced_elsewhere() { assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false)); - model.set_cell_empty(0, 1, 1).unwrap(); + model.cell_clear_contents(0, 1, 1).unwrap(); model.evaluate(); assert_eq!(model._get_text_at(0, 1, 1), ""); diff --git a/base/src/test/test_column_width.rs b/base/src/test/test_column_width.rs index beee9cd..3e38eb0 100644 --- a/base/src/test/test_column_width.rs +++ b/base/src/test/test_column_width.rs @@ -23,9 +23,9 @@ fn test_column_width() { .unwrap(); assert_eq!(model.workbook.worksheets[0].cols.len(), 3); let worksheet = model.workbook.worksheet(0).unwrap(); - assert!((worksheet.column_width(1).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); - assert!((worksheet.column_width(2).unwrap() - 30.0).abs() < f64::EPSILON); - assert!((worksheet.column_width(3).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); + assert!((worksheet.get_column_width(1).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); + assert!((worksheet.get_column_width(2).unwrap() - 30.0).abs() < f64::EPSILON); + assert!((worksheet.get_column_width(3).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); assert_eq!(model.get_cell_style_index(0, 23, 2), 6); } @@ -48,9 +48,11 @@ fn test_column_width_lower_edge() { .unwrap(); assert_eq!(model.workbook.worksheets[0].cols.len(), 2); let worksheet = model.workbook.worksheet(0).unwrap(); - assert!((worksheet.column_width(4).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); - assert!((worksheet.column_width(5).unwrap() - 30.0).abs() < f64::EPSILON); - assert!((worksheet.column_width(6).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON); + assert!((worksheet.get_column_width(4).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); + assert!((worksheet.get_column_width(5).unwrap() - 30.0).abs() < f64::EPSILON); + assert!( + (worksheet.get_column_width(6).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON + ); assert_eq!(model.get_cell_style_index(0, 23, 5), 1); } @@ -74,9 +76,9 @@ fn test_column_width_higher_edge() { assert_eq!(model.workbook.worksheets[0].cols.len(), 2); let worksheet = model.workbook.worksheet(0).unwrap(); assert!( - (worksheet.column_width(15).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON + (worksheet.get_column_width(15).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON ); - assert!((worksheet.column_width(16).unwrap() - 30.0).abs() < f64::EPSILON); - assert!((worksheet.column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); + assert!((worksheet.get_column_width(16).unwrap() - 30.0).abs() < f64::EPSILON); + assert!((worksheet.get_column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON); assert_eq!(model.get_cell_style_index(0, 23, 16), 1); } diff --git a/base/src/test/test_frozen_rows_and_columns.rs b/base/src/test/test_frozen_rows_and_columns.rs index be812c2..8919685 100644 --- a/base/src/test/test_frozen_rows_and_columns.rs +++ b/base/src/test/test_frozen_rows_and_columns.rs @@ -8,34 +8,37 @@ use crate::{ #[test] fn test_empty_model() { let mut model = new_empty_model(); - assert_eq!(model.get_frozen_rows(0), Ok(0)); - assert_eq!(model.get_frozen_columns(0), Ok(0)); + assert_eq!(model.get_frozen_rows_count(0), Ok(0)); + assert_eq!(model.get_frozen_columns_count(0), Ok(0)); let e = model.set_frozen_rows(0, 3); assert!(e.is_ok()); - assert_eq!(model.get_frozen_rows(0), Ok(3)); - assert_eq!(model.get_frozen_columns(0), Ok(0)); + assert_eq!(model.get_frozen_rows_count(0), Ok(3)); + assert_eq!(model.get_frozen_columns_count(0), Ok(0)); let e = model.set_frozen_columns(0, 53); assert!(e.is_ok()); - assert_eq!(model.get_frozen_rows(0), Ok(3)); - assert_eq!(model.get_frozen_columns(0), Ok(53)); + assert_eq!(model.get_frozen_rows_count(0), Ok(3)); + assert_eq!(model.get_frozen_columns_count(0), Ok(53)); // Set them back to zero let e = model.set_frozen_rows(0, 0); assert!(e.is_ok()); let e = model.set_frozen_columns(0, 0); assert!(e.is_ok()); - assert_eq!(model.get_frozen_rows(0), Ok(0)); - assert_eq!(model.get_frozen_columns(0), Ok(0)); + assert_eq!(model.get_frozen_rows_count(0), Ok(0)); + assert_eq!(model.get_frozen_columns_count(0), Ok(0)); } #[test] fn test_invalid_sheet() { let mut model = new_empty_model(); - assert_eq!(model.get_frozen_rows(1), Err("Invalid sheet".to_string())); assert_eq!( - model.get_frozen_columns(3), + model.get_frozen_rows_count(1), + Err("Invalid sheet".to_string()) + ); + assert_eq!( + model.get_frozen_columns_count(3), Err("Invalid sheet".to_string()) ); diff --git a/base/src/test/test_general.rs b/base/src/test/test_general.rs index ada2ea8..58a583d 100644 --- a/base/src/test/test_general.rs +++ b/base/src/test/test_general.rs @@ -411,11 +411,11 @@ fn test_get_formatted_cell_value() { model.evaluate(); - assert_eq!(model.formatted_cell_value(0, 1, 1).unwrap(), "foobar"); - assert_eq!(model.formatted_cell_value(0, 2, 1).unwrap(), "TRUE"); - assert_eq!(model.formatted_cell_value(0, 3, 1).unwrap(), ""); - assert_eq!(model.formatted_cell_value(0, 4, 1).unwrap(), "123.456"); - assert_eq!(model.formatted_cell_value(0, 5, 1).unwrap(), "$123.46"); + assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "foobar"); + assert_eq!(model.get_formatted_cell_value(0, 2, 1).unwrap(), "TRUE"); + assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), ""); + assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "123.456"); + assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "$123.46"); } #[test] @@ -426,20 +426,20 @@ fn test_cell_formula() { model.evaluate(); assert_eq!( - model.cell_formula(0, 1, 1), // A1 + model.get_cell_formula(0, 1, 1), // A1 Ok(Some("=1+2+3".to_string())), ); assert_eq!( - model.cell_formula(0, 2, 1), // A2 + model.get_cell_formula(0, 2, 1), // A2 Ok(None), ); assert_eq!( - model.cell_formula(0, 3, 1), // A3 - empty cell + model.get_cell_formula(0, 3, 1), // A3 - empty cell Ok(None), ); assert_eq!( - model.cell_formula(42, 1, 1), + model.get_cell_formula(42, 1, 1), Err("Invalid sheet index".to_string()), ); } @@ -453,16 +453,16 @@ fn test_xlfn() { model.evaluate(); // Only modern formulas strip the '_xlfn.' assert_eq!( - model.cell_formula(0, 1, 1).unwrap(), + model.get_cell_formula(0, 1, 1).unwrap(), Some("=_xlfn.SIN(1)".to_string()) ); // unknown formulas keep the '_xlfn.' prefix assert_eq!( - model.cell_formula(0, 2, 1).unwrap(), + model.get_cell_formula(0, 2, 1).unwrap(), Some("=_xlfn.SINY(1)".to_string()) ); assert_eq!( - model.cell_formula(0, 3, 1).unwrap(), + model.get_cell_formula(0, 3, 1).unwrap(), Some("=CONCAT(3,4)".to_string()) ); } @@ -474,11 +474,11 @@ fn test_letter_case() { model._set("A2", "=sIn(2)"); model.evaluate(); assert_eq!( - model.cell_formula(0, 1, 1).unwrap(), + model.get_cell_formula(0, 1, 1).unwrap(), Some("=SIN(1)".to_string()) ); assert_eq!( - model.cell_formula(0, 2, 1).unwrap(), + model.get_cell_formula(0, 2, 1).unwrap(), Some("=SIN(2)".to_string()) ); } diff --git a/base/src/test/test_model_delete_cell.rs b/base/src/test/test_model_cell_clear_all.rs similarity index 76% rename from base/src/test/test_model_delete_cell.rs rename to base/src/test/test_model_cell_clear_all.rs index 8ba8472..f1ec540 100644 --- a/base/src/test/test_model_delete_cell.rs +++ b/base/src/test/test_model_cell_clear_all.rs @@ -2,22 +2,22 @@ use crate::test::util::new_empty_model; #[test] -fn test_delete_cell_non_existing_sheet() { +fn test_cell_clear_all_non_existing_sheet() { let mut model = new_empty_model(); assert_eq!( - model.delete_cell(13, 1, 1), + model.cell_clear_all(13, 1, 1), Err("Invalid sheet index".to_string()) ); } #[test] -fn test_delete_cell_unset_cell() { +fn test_cell_clear_all_unset_cell() { let mut model = new_empty_model(); - assert!(model.delete_cell(0, 1, 1).is_ok()); + assert!(model.cell_clear_all(0, 1, 1).is_ok()); } #[test] -fn test_delete_cell_with_value() { +fn test_cell_clear_all_with_value() { let mut model = new_empty_model(); model._set("A1", "hello"); model.evaluate(); @@ -25,7 +25,7 @@ fn test_delete_cell_with_value() { assert_eq!(model._get_text_at(0, 1, 1), "hello"); assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); - model.delete_cell(0, 1, 1).unwrap(); + model.cell_clear_all(0, 1, 1).unwrap(); model.evaluate(); assert_eq!(model._get_text_at(0, 1, 1), ""); @@ -33,7 +33,7 @@ fn test_delete_cell_with_value() { } #[test] -fn test_delete_cell_referenced_elsewhere() { +fn test_cell_clear_all_referenced_elsewhere() { let mut model = new_empty_model(); model._set("A1", "35"); model._set("A2", "=2*A1"); @@ -44,7 +44,7 @@ fn test_delete_cell_referenced_elsewhere() { assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false)); - model.delete_cell(0, 1, 1).unwrap(); + model.cell_clear_all(0, 1, 1).unwrap(); model.evaluate(); assert_eq!(model._get_text_at(0, 1, 1), ""); diff --git a/base/src/test/test_model_is_empty_cell.rs b/base/src/test/test_model_is_empty_cell.rs index 888823d..0db431b 100644 --- a/base/src/test/test_model_is_empty_cell.rs +++ b/base/src/test/test_model_is_empty_cell.rs @@ -16,7 +16,7 @@ fn test_is_empty_cell() { assert!(model.is_empty_cell(0, 3, 1).unwrap()); model.set_user_input(0, 3, 1, "Hello World".to_string()); assert!(!model.is_empty_cell(0, 3, 1).unwrap()); - model.set_cell_empty(0, 3, 1).unwrap(); + model.cell_clear_contents(0, 3, 1).unwrap(); assert!(model.is_empty_cell(0, 3, 1).unwrap()); } diff --git a/base/src/test/test_sheet_markup.rs b/base/src/test/test_sheet_markup.rs index 460244b..3403bfe 100644 --- a/base/src/test/test_sheet_markup.rs +++ b/base/src/test/test_sheet_markup.rs @@ -21,7 +21,7 @@ fn test_sheet_markup() { model.set_cell_style(0, 4, 1, &style).unwrap(); assert_eq!( - model.sheet_markup(0), + model.get_sheet_markup(0), Ok("**Item**|**Cost**\nRent|$600\nElectricity|$200\n**Total**|=SUM(B2:B3)".to_string()), ) } diff --git a/base/src/test/test_sheets.rs b/base/src/test/test_sheets.rs index 6d73171..63f4c4d 100644 --- a/base/src/test/test_sheets.rs +++ b/base/src/test/test_sheets.rs @@ -236,3 +236,11 @@ fn test_delete_sheet_by_index() { assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2"]); assert_eq!(model._get_text("Sheet2!A1"), "#REF!"); } + +#[test] +fn delete_sheet_error() { + let mut model = new_empty_model(); + model.new_sheet(); + assert!(model.delete_sheet(2).is_err()); + assert!(model.delete_sheet(1).is_ok()); +} diff --git a/base/src/test/test_workbook.rs b/base/src/test/test_workbook.rs index a50771f..11d490e 100644 --- a/base/src/test/test_workbook.rs +++ b/base/src/test/test_workbook.rs @@ -5,7 +5,7 @@ use crate::{test::util::new_empty_model, types::SheetInfo}; #[test] fn workbook_worksheets_info() { let model = new_empty_model(); - let sheets_info = model.workbook.get_worksheets_info(); + let sheets_info = model.get_sheets_info(); assert_eq!( sheets_info[0], SheetInfo { diff --git a/base/src/test/test_worksheet.rs b/base/src/test/test_worksheet.rs index bb3670d..da0a8ec 100644 --- a/base/src/test/test_worksheet.rs +++ b/base/src/test/test_worksheet.rs @@ -39,7 +39,7 @@ fn test_worksheet_dimension_single_cell() { fn test_worksheet_dimension_single_cell_set_empty() { let mut model = new_empty_model(); model._set("W11", "1"); - model.set_cell_empty(0, 11, 23).unwrap(); + model.cell_clear_contents(0, 11, 23).unwrap(); assert_eq!( model.workbook.worksheet(0).unwrap().dimension(), WorksheetDimension { @@ -55,7 +55,7 @@ fn test_worksheet_dimension_single_cell_set_empty() { fn test_worksheet_dimension_single_cell_deleted() { let mut model = new_empty_model(); model._set("W11", "1"); - model.delete_cell(0, 11, 23).unwrap(); + model.cell_clear_all(0, 11, 23).unwrap(); assert_eq!( model.workbook.worksheet(0).unwrap().dimension(), WorksheetDimension { @@ -75,7 +75,7 @@ fn test_worksheet_dimension_multiple_cells() { model._set("AA17", "1"); model._set("G17", "1"); model._set("B19", "1"); - model.delete_cell(0, 11, 23).unwrap(); + model.cell_clear_all(0, 11, 23).unwrap(); assert_eq!( model.workbook.worksheet(0).unwrap().dimension(), WorksheetDimension { diff --git a/base/src/test/user_model/mod.rs b/base/src/test/user_model/mod.rs new file mode 100644 index 0000000..e8165a4 --- /dev/null +++ b/base/src/test/user_model/mod.rs @@ -0,0 +1,9 @@ +mod test_add_delete_sheets; +mod test_clear_cells; +mod test_diff_queue; +mod test_evaluation; +mod test_general; +mod test_rename_sheet; +mod test_row_column; +mod test_styles; +mod test_undo_redo; diff --git a/base/src/test/user_model/test_add_delete_sheets.rs b/base/src/test/user_model/test_add_delete_sheets.rs new file mode 100644 index 0000000..52cb183 --- /dev/null +++ b/base/src/test/user_model/test_add_delete_sheets.rs @@ -0,0 +1,58 @@ +#![allow(clippy::unwrap_used)] + +use crate::{constants::DEFAULT_COLUMN_WIDTH, UserModel}; + +#[test] +fn add_undo_redo() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.new_sheet(); + model.set_user_input(1, 1, 1, "=1 + 1").unwrap(); + model.set_user_input(1, 1, 2, "=A1*3").unwrap(); + model + .set_column_width(1, 5, 5.0 * DEFAULT_COLUMN_WIDTH) + .unwrap(); + model.new_sheet(); + model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap(); + + model.undo().unwrap(); + model.undo().unwrap(); + + assert!(model.get_formatted_cell_value(2, 1, 1).is_err()); + + model.redo().unwrap(); + model.redo().unwrap(); + + assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string())); + + model.delete_sheet(1).unwrap(); + + assert!(!model.can_undo()); + assert!(!model.can_redo()); +} + +#[test] +fn new_sheet_propagates() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.new_sheet(); + + let send_queue = model.flush_send_queue(); + + let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap(); + model2.apply_external_diffs(&send_queue).unwrap(); + let sheets_info = model2.get_sheets_info(); + assert_eq!(sheets_info.len(), 2); +} + +#[test] +fn delete_sheet_propagates() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.new_sheet(); + model.delete_sheet(0).unwrap(); + + let send_queue = model.flush_send_queue(); + + let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap(); + model2.apply_external_diffs(&send_queue).unwrap(); + let sheets_info = model2.get_sheets_info(); + assert_eq!(sheets_info.len(), 1); +} diff --git a/base/src/test/user_model/test_clear_cells.rs b/base/src/test/user_model/test_clear_cells.rs new file mode 100644 index 0000000..ef0685c --- /dev/null +++ b/base/src/test/user_model/test_clear_cells.rs @@ -0,0 +1,91 @@ +#![allow(clippy::unwrap_used)] + +use crate::{expressions::types::Area, UserModel}; + +#[test] +fn basic() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.set_user_input(0, 1, 1, "100$").unwrap(); + 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, 1), Ok("".to_string())); + model.undo().unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 1), + Ok("100$".to_string()) + ); + model.redo().unwrap(); + assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string())); + + model.set_user_input(0, 1, 1, "300").unwrap(); + // clear contents keeps the formatting + assert_eq!( + model.get_formatted_cell_value(0, 1, 1), + Ok("300$".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, 1), Ok("".to_string())); + model.undo().unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 1), + Ok("300$".to_string()) + ); + model.redo().unwrap(); + assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string())); + model.set_user_input(0, 1, 1, "400").unwrap(); + // clear contents keeps the formatting + assert_eq!( + model.get_formatted_cell_value(0, 1, 1), + Ok("400".to_string()) + ); +} + +#[test] +fn clear_empty_cell() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + 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, 1), Ok("".to_string())); + model.undo().unwrap(); + assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string())); +} + +#[test] +fn clear_all_empty_cell() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + 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, 1), Ok("".to_string())); + model.undo().unwrap(); + assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string())); +} diff --git a/base/src/test/user_model/test_diff_queue.rs b/base/src/test/user_model/test_diff_queue.rs new file mode 100644 index 0000000..147ce93 --- /dev/null +++ b/base/src/test/user_model/test_diff_queue.rs @@ -0,0 +1,159 @@ +use crate::{ + constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT}, + test::util::new_empty_model, + UserModel, +}; + +#[test] +fn send_queue() { + let mut model1 = UserModel::from_model(new_empty_model()); + let width = model1.get_column_width(0, 3).unwrap() * 3.0; + model1.set_column_width(0, 3, width).unwrap(); + model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap(); + let send_queue = model1.flush_send_queue(); + + let mut model2 = UserModel::from_model(new_empty_model()); + model2.apply_external_diffs(&send_queue).unwrap(); + + assert_eq!(model2.get_column_width(0, 3), Ok(width)); + assert_eq!( + model2.get_formatted_cell_value(0, 1, 2), + Ok("Hello IronCalc!".to_string()) + ); +} + +#[test] +fn apply_external_diffs_wrong_str() { + let mut model1 = UserModel::from_model(new_empty_model()); + assert!(model1.apply_external_diffs("invalid").is_err()); +} + +#[test] +fn queue_undo_redo() { + let mut model1 = UserModel::from_model(new_empty_model()); + let width = model1.get_column_width(0, 3).unwrap() * 3.0; + model1.set_column_width(0, 3, width).unwrap(); + model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap(); + assert!(model1.undo().is_ok()); + assert!(model1.redo().is_ok()); + let send_queue = model1.flush_send_queue(); + + let mut model2 = UserModel::from_model(new_empty_model()); + model2.apply_external_diffs(&send_queue).unwrap(); + + assert_eq!(model2.get_column_width(0, 3), Ok(width)); + assert_eq!( + model2.get_formatted_cell_value(0, 1, 2), + Ok("Hello IronCalc!".to_string()) + ); +} + +#[test] +fn queue_undo_redo_multiple() { + let mut model1 = UserModel::from_model(new_empty_model()); + + // do a bunch of things + model1.set_frozen_columns_count(0, 5).unwrap(); + model1.set_frozen_rows_count(0, 6).unwrap(); + model1.set_column_width(0, 7, 300.0).unwrap(); + model1.set_row_height(0, 23, 123.0).unwrap(); + model1.set_user_input(0, 55, 55, "=42+8").unwrap(); + + for row in 1..5 { + model1.set_user_input(0, row, 17, "=ROW()").unwrap(); + } + + model1.insert_row(0, 3).unwrap(); + model1.insert_row(0, 3).unwrap(); + + // undo al of them + while model1.can_undo() { + model1.undo().unwrap(); + } + + // check it is an empty model + assert_eq!(model1.get_frozen_columns_count(0), Ok(0)); + assert_eq!(model1.get_frozen_rows_count(0), Ok(0)); + assert_eq!(model1.get_column_width(0, 7), Ok(DEFAULT_COLUMN_WIDTH)); + assert_eq!( + model1.get_formatted_cell_value(0, 55, 55), + Ok("".to_string()) + ); + assert_eq!(model1.get_row_height(0, 23), Ok(DEFAULT_ROW_HEIGHT)); + assert_eq!( + model1.get_formatted_cell_value(0, 57, 55), + Ok("".to_string()) + ); + assert_eq!(model1.get_row_height(0, 25), Ok(DEFAULT_ROW_HEIGHT)); + + // redo all of them + while model1.can_redo() { + model1.redo().unwrap(); + } + + // now send all this to a new model + let send_queue = model1.flush_send_queue(); + let mut model2 = UserModel::from_model(new_empty_model()); + model2.apply_external_diffs(&send_queue).unwrap(); + + // Check everything is as expected + assert_eq!(model2.get_frozen_columns_count(0), Ok(5)); + assert_eq!(model2.get_frozen_rows_count(0), Ok(6)); + assert_eq!(model2.get_column_width(0, 7), Ok(300.0)); + // I inserted two rows + assert_eq!( + model2.get_formatted_cell_value(0, 57, 55), + Ok("50".to_string()) + ); + assert_eq!(model2.get_row_height(0, 25), Ok(123.0)); + + assert_eq!( + model2.get_formatted_cell_value(0, 1, 17), + Ok("1".to_string()) + ); + assert_eq!( + model2.get_formatted_cell_value(0, 2, 17), + Ok("2".to_string()) + ); + + assert_eq!( + model2.get_formatted_cell_value(0, 3, 17), + Ok("".to_string()) + ); + assert_eq!( + model2.get_formatted_cell_value(0, 4, 17), + Ok("".to_string()) + ); + + assert_eq!( + model2.get_formatted_cell_value(0, 5, 17), + Ok("5".to_string()) + ); + assert_eq!( + model2.get_formatted_cell_value(0, 6, 17), + Ok("6".to_string()) + ); +} + +#[test] +fn new_sheet() { + let mut model1 = UserModel::from_model(new_empty_model()); + model1.new_sheet(); + model1.set_user_input(0, 1, 1, "42").unwrap(); + model1.set_user_input(1, 1, 1, "=Sheet1!A1*2").unwrap(); + + let send_queue = model1.flush_send_queue(); + let mut model2 = UserModel::from_model(new_empty_model()); + model2.apply_external_diffs(&send_queue).unwrap(); + + assert_eq!( + model2.get_formatted_cell_value(1, 1, 1), + Ok("84".to_string()) + ); +} + +#[test] +fn wrong_diffs_handled() { + let mut model = UserModel::from_model(new_empty_model()); + assert!(model.apply_external_diffs("Hello world").is_err()); +} diff --git a/base/src/test/user_model/test_evaluation.rs b/base/src/test/user_model/test_evaluation.rs new file mode 100644 index 0000000..1c148e4 --- /dev/null +++ b/base/src/test/user_model/test_evaluation.rs @@ -0,0 +1,31 @@ +#![allow(clippy::unwrap_used)] + +use crate::UserModel; + +#[test] +fn model_evaluates_automatically() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.set_user_input(0, 1, 1, "=1 + 1").unwrap(); + + assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("2".to_string())); + assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+1".to_string())); +} + +#[test] +fn pause_resume_evaluation() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.pause_evaluation(); + model.set_user_input(0, 1, 1, "=1+1").unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 1), + Ok("#ERROR!".to_string()) + ); + model.evaluate(); + + assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("2".to_string())); + assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+1".to_string())); + + model.resume_evaluation(); + model.set_user_input(0, 2, 1, "=1+4").unwrap(); + assert_eq!(model.get_formatted_cell_value(0, 2, 1), Ok("5".to_string())); +} diff --git a/base/src/test/user_model/test_general.rs b/base/src/test/user_model/test_general.rs new file mode 100644 index 0000000..0cd0d7d --- /dev/null +++ b/base/src/test/user_model/test_general.rs @@ -0,0 +1,103 @@ +#![allow(clippy::unwrap_used)] + +use crate::constants::{LAST_COLUMN, LAST_ROW}; +use crate::test::util::new_empty_model; +use crate::UserModel; + +#[test] +fn set_user_input_errors() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // Wrong sheet + assert!(model.set_user_input(1, 1, 1, "1").is_err()); + // Wrong row + assert!(model.set_user_input(0, 0, 1, "1").is_err()); + // Wrong column + assert!(model.set_user_input(0, 1, 0, "1").is_err()); + // row too large + assert!(model.set_user_input(0, LAST_ROW, 1, "1").is_ok()); + assert!(model.set_user_input(0, LAST_ROW + 1, 1, "1").is_err()); + // column too large + assert!(model.set_user_input(0, 1, LAST_COLUMN, "1").is_ok()); + assert!(model.set_user_input(0, 1, LAST_COLUMN + 1, "1").is_err()); +} + +#[test] +fn insert_remove_rows() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + let height = model.get_row_height(0, 5).unwrap(); + + // Insert some data in row 5 (and change the style) + assert!(model.set_user_input(0, 5, 1, "100$").is_ok()); + // Change the height of the column + assert!(model.set_row_height(0, 5, 3.0 * height).is_ok()); + + // remove the row + assert!(model.delete_row(0, 5).is_ok()); + // Row 5 has now the normal height + assert_eq!(model.get_row_height(0, 5), Ok(height)); + // There is no value in A5 + assert_eq!(model.get_formatted_cell_value(0, 5, 1), Ok("".to_string())); + // Setting a value will not format it + assert!(model.set_user_input(0, 5, 1, "125").is_ok()); + assert_eq!( + model.get_formatted_cell_value(0, 5, 1), + Ok("125".to_string()) + ); + + // undo twice + assert!(model.undo().is_ok()); + assert!(model.undo().is_ok()); + + assert_eq!(model.get_row_height(0, 5), Ok(3.0 * height)); + assert_eq!( + model.get_formatted_cell_value(0, 5, 1), + Ok("100$".to_string()) + ); +} + +#[test] +fn insert_remove_columns() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // column E + let column_width = model.get_column_width(0, 5).unwrap(); + println!("{column_width}"); + + // Insert some data in row 5 (and change the style) in E1 + assert!(model.set_user_input(0, 1, 5, "100$").is_ok()); + // Change the width of the column + assert!(model.set_column_width(0, 5, 3.0 * column_width).is_ok()); + assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width); + + // remove the column + assert!(model.delete_column(0, 5).is_ok()); + // Column 5 has now the normal width + assert_eq!(model.get_column_width(0, 5), Ok(column_width)); + // There is no value in E5 + assert_eq!(model.get_formatted_cell_value(0, 1, 5), Ok("".to_string())); + // Setting a value will not format it + assert!(model.set_user_input(0, 1, 5, "125").is_ok()); + assert_eq!( + model.get_formatted_cell_value(0, 1, 5), + Ok("125".to_string()) + ); + + // undo twice (set_user_input and delete_column) + assert!(model.undo().is_ok()); + assert!(model.undo().is_ok()); + + assert_eq!(model.get_column_width(0, 5), Ok(3.0 * column_width)); + assert_eq!( + model.get_formatted_cell_value(0, 1, 5), + Ok("100$".to_string()) + ); +} + +#[test] +fn delete_remove_cell() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let (sheet, row, column) = (0, 1, 1); + model.set_user_input(sheet, row, column, "100$").unwrap(); +} diff --git a/base/src/test/user_model/test_rename_sheet.rs b/base/src/test/user_model/test_rename_sheet.rs new file mode 100644 index 0000000..cc02c5f --- /dev/null +++ b/base/src/test/user_model/test_rename_sheet.rs @@ -0,0 +1,39 @@ +#![allow(clippy::unwrap_used)] + +use crate::UserModel; + +#[test] +fn basic_rename() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.rename_sheet(0, "NewSheet").unwrap(); + assert_eq!(model.get_sheets_info()[0].name, "NewSheet"); +} + +#[test] +fn undo_redo() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + model.rename_sheet(0, "NewSheet").unwrap(); + model.undo().unwrap(); + assert_eq!(model.get_sheets_info()[0].name, "Sheet1"); + model.redo().unwrap(); + assert_eq!(model.get_sheets_info()[0].name, "NewSheet"); + + let send_queue = model.flush_send_queue(); + + let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap(); + model2.apply_external_diffs(&send_queue).unwrap(); + assert_eq!(model.get_sheets_info()[0].name, "NewSheet"); +} + +#[test] +fn errors() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + assert_eq!( + model.rename_sheet(0, ""), + Err("Invalid name for a sheet: ''.".to_string()) + ); + assert_eq!( + model.rename_sheet(1, "Hello"), + Err("Invalid sheet index".to_string()) + ); +} diff --git a/base/src/test/user_model/test_row_column.rs b/base/src/test/user_model/test_row_column.rs new file mode 100644 index 0000000..9d00b92 --- /dev/null +++ b/base/src/test/user_model/test_row_column.rs @@ -0,0 +1,156 @@ +#![allow(clippy::unwrap_used)] + +use crate::{ + constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN}, + test::util::new_empty_model, + UserModel, +}; + +#[test] +fn simple_insert_row() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + let (sheet, column) = (0, 5); + for row in 1..5 { + assert!(model.set_user_input(sheet, row, column, "123").is_ok()); + } + assert!(model.insert_row(sheet, 3).is_ok()); + assert_eq!( + model.get_formatted_cell_value(sheet, 3, column).unwrap(), + "" + ); + + assert!(model.undo().is_ok()); + assert_eq!( + model.get_formatted_cell_value(sheet, 3, column).unwrap(), + "123" + ); + assert!(model.redo().is_ok()); + assert_eq!( + model.get_formatted_cell_value(sheet, 3, column).unwrap(), + "" + ); +} + +#[test] +fn simple_insert_column() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + let (sheet, row) = (0, 5); + for column in 1..5 { + assert!(model.set_user_input(sheet, row, column, "123").is_ok()); + } + assert!(model.insert_column(sheet, 3).is_ok()); + assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), ""); + + assert!(model.undo().is_ok()); + assert_eq!( + model.get_formatted_cell_value(sheet, row, 3).unwrap(), + "123" + ); + assert!(model.redo().is_ok()); + assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), ""); +} + +#[test] +fn simple_delete_column() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + model.set_user_input(0, 1, 5, "3").unwrap(); + model.set_user_input(0, 2, 5, "=E1*2").unwrap(); + model + .set_column_width(0, 5, DEFAULT_COLUMN_WIDTH * 3.0) + .unwrap(); + + model.delete_column(0, 5).unwrap(); + + assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string())); + assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH)); + + model.undo().unwrap(); + + assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("6".to_string())); + assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH * 3.0)); + + let send_queue = model.flush_send_queue(); + + let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap(); + model2.apply_external_diffs(&send_queue).unwrap(); + + assert_eq!( + model2.get_formatted_cell_value(0, 2, 5), + Ok("6".to_string()) + ); + assert_eq!( + model2.get_column_width(0, 5), + Ok(DEFAULT_COLUMN_WIDTH * 3.0) + ); +} + +#[test] +fn delete_column_errors() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + assert_eq!( + model.delete_column(1, 1), + Err("Invalid sheet index".to_string()) + ); + + assert_eq!( + model.delete_column(0, 0), + Err("Column number '0' is not valid.".to_string()) + ); + assert_eq!( + model.delete_column(0, LAST_COLUMN + 1), + Err("Column number '16385' is not valid.".to_string()) + ); + + assert_eq!(model.delete_column(0, LAST_COLUMN), Ok(())); +} + +#[test] +fn simple_delete_row() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + model.set_user_input(0, 15, 4, "3").unwrap(); + model.set_user_input(0, 15, 6, "=D15*2").unwrap(); + + model + .set_row_height(0, 15, DEFAULT_ROW_HEIGHT * 3.0) + .unwrap(); + + model.delete_row(0, 15).unwrap(); + + assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string())); + assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT)); + + model.undo().unwrap(); + + assert_eq!( + model.get_formatted_cell_value(0, 15, 6), + Ok("6".to_string()) + ); + assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT * 3.0)); + + let send_queue = model.flush_send_queue(); + + let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap(); + model2.apply_external_diffs(&send_queue).unwrap(); + + assert_eq!( + model2.get_formatted_cell_value(0, 15, 6), + Ok("6".to_string()) + ); + assert_eq!(model2.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT * 3.0)); +} + +#[test] +fn simple_delete_row_no_style() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + model.set_user_input(0, 15, 4, "3").unwrap(); + model.set_user_input(0, 15, 6, "=D15*2").unwrap(); + model.delete_row(0, 15).unwrap(); + + assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string())); +} diff --git a/base/src/test/user_model/test_styles.rs b/base/src/test/user_model/test_styles.rs new file mode 100644 index 0000000..265ce3a --- /dev/null +++ b/base/src/test/user_model/test_styles.rs @@ -0,0 +1,711 @@ +#![allow(clippy::unwrap_used)] + +use crate::{ + expressions::types::Area, + types::{Alignment, BorderItem, BorderStyle, HorizontalAlignment, VerticalAlignment}, + UserModel, +}; + +#[test] +fn basic_fonts() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(!style.font.i); + assert!(!style.font.b); + assert!(!style.font.u); + assert!(!style.font.strike); + assert_eq!(style.font.color, Some("#000000".to_owned())); + + // bold + model.update_range_style(&range, "font.b", "true").unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(style.font.b); + + // italics + model.update_range_style(&range, "font.i", "true").unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(style.font.i); + + // underline + model.update_range_style(&range, "font.u", "true").unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(style.font.u); + + // strike + model + .update_range_style(&range, "font.strike", "true") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(style.font.strike); + + // color + model + .update_range_style(&range, "font.color", "#F1F1F1") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.font.color, Some("#F1F1F1".to_owned())); + + while model.can_undo() { + model.undo().unwrap(); + } + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(!style.font.i); + assert!(!style.font.b); + assert!(!style.font.u); + assert!(!style.font.strike); + assert_eq!(style.font.color, Some("#000000".to_owned())); + + while model.can_redo() { + model.redo().unwrap(); + } + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(style.font.i); + assert!(style.font.b); + assert!(style.font.u); + assert!(style.font.strike); + assert_eq!(style.font.color, Some("#F1F1F1".to_owned())); + + let send_queue = model.flush_send_queue(); + + let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap(); + model2.apply_external_diffs(&send_queue).unwrap(); + + let style = model2.get_cell_style(0, 1, 1).unwrap(); + assert!(style.font.i); + assert!(style.font.b); + assert!(style.font.u); + assert!(style.font.strike); + assert_eq!(style.font.color, Some("#F1F1F1".to_owned())); +} + +#[test] +fn font_errors() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + assert_eq!( + model.update_range_style(&range, "font.b", "True"), + Err("Invalid value for boolean: 'True'.".to_string()) + ); + assert_eq!( + model.update_range_style(&range, "font.i", "FALSE"), + Err("Invalid value for boolean: 'FALSE'.".to_string()) + ); + assert_eq!( + model.update_range_style(&range, "font.bold", "true"), + Err("Invalid style path: 'font.bold'.".to_string()) + ); + assert_eq!( + model.update_range_style(&range, "font.strike", ""), + Err("Invalid value for boolean: ''.".to_string()) + ); + // There is no cast for booleans + assert_eq!( + model.update_range_style(&range, "font.b", "1"), + Err("Invalid value for boolean: '1'.".to_string()) + ); + // colors don't work by name + assert_eq!( + model.update_range_style(&range, "font.color", "blue"), + Err("Invalid color: 'blue'.".to_string()) + ); + // No short form + assert_eq!( + model.update_range_style(&range, "font.color", "#FFF"), + Err("Invalid color: '#FFF'.".to_string()) + ); +} + +#[test] +fn basic_fill() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.fill.bg_color, None); + + // bg_color + model + .update_range_style(&range, "fill.bg_color", "#F2F2F2") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned())); + + let send_queue = model.flush_send_queue(); + + let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap(); + model2.apply_external_diffs(&send_queue).unwrap(); + + let style = model2.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned())); +} + +#[test] +fn fill_errors() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + assert!(model + .update_range_style(&range, "fill.bg_color", "#FFF") + .is_err()); +} + +#[test] +fn basic_format() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.num_fmt, "general"); + + model + .update_range_style(&range, "num_fmt", "$#,##0.0000") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.num_fmt, "$#,##0.0000"); + + model.undo().unwrap(); + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.num_fmt, "general"); + + model.redo().unwrap(); + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.num_fmt, "$#,##0.0000"); + + let send_queue = model.flush_send_queue(); + + let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap(); + model2.apply_external_diffs(&send_queue).unwrap(); + + let style = model2.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.num_fmt, "$#,##0.0000"); +} + +#[test] +fn basic_borders() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + + model + .update_range_style(&range, "border.left", "thin,#F1F1F1") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::Thin, + color: Some("#F1F1F1".to_owned()), + }) + ); + + model + .update_range_style(&range, "border.left", "thin,") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::Thin, + color: None, + }) + ); + + model + .update_range_style(&range, "border.right", "dotted,#F1F1F2") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.right, + Some(BorderItem { + style: BorderStyle::Dotted, + color: Some("#F1F1F2".to_owned()), + }) + ); + + model + .update_range_style(&range, "border.top", "double,#F1F1F3") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.top, + Some(BorderItem { + style: BorderStyle::Double, + color: Some("#F1F1F3".to_owned()), + }) + ); + + model + .update_range_style(&range, "border.bottom", "medium,#F1F1F4") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.bottom, + Some(BorderItem { + style: BorderStyle::Medium, + color: Some("#F1F1F4".to_owned()), + }) + ); + + while model.can_undo() { + model.undo().unwrap(); + } + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.border.left, None); + assert_eq!(style.border.top, None); + assert_eq!(style.border.right, None); + assert_eq!(style.border.bottom, None); + + while model.can_redo() { + model.redo().unwrap(); + } + + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::Thin, + color: None, + }) + ); + assert_eq!( + style.border.right, + Some(BorderItem { + style: BorderStyle::Dotted, + color: Some("#F1F1F2".to_owned()), + }) + ); + assert_eq!( + style.border.top, + Some(BorderItem { + style: BorderStyle::Double, + color: Some("#F1F1F3".to_owned()), + }) + ); + assert_eq!( + style.border.bottom, + Some(BorderItem { + style: BorderStyle::Medium, + color: Some("#F1F1F4".to_owned()), + }) + ); + + let send_queue = model.flush_send_queue(); + + let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap(); + model2.apply_external_diffs(&send_queue).unwrap(); + + let style = model2.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::Thin, + color: None, + }) + ); + assert_eq!( + style.border.right, + Some(BorderItem { + style: BorderStyle::Dotted, + color: Some("#F1F1F2".to_owned()), + }) + ); + assert_eq!( + style.border.top, + Some(BorderItem { + style: BorderStyle::Double, + color: Some("#F1F1F3".to_owned()), + }) + ); + assert_eq!( + style.border.bottom, + Some(BorderItem { + style: BorderStyle::Medium, + color: Some("#F1F1F4".to_owned()), + }) + ); +} + +#[test] +fn basic_alignment() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + + let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment; + assert_eq!(alignment, None); + + model + .update_range_style(&range, "alignment.horizontal", "center") + .unwrap(); + let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment; + assert_eq!( + alignment, + Some(Alignment { + horizontal: HorizontalAlignment::Center, + vertical: VerticalAlignment::Bottom, + wrap_text: false + }) + ); + + model + .update_range_style(&range, "alignment.horizontal", "centerContinuous") + .unwrap(); + let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment; + assert_eq!( + alignment, + Some(Alignment { + horizontal: HorizontalAlignment::CenterContinuous, + vertical: VerticalAlignment::Bottom, + wrap_text: false + }) + ); + + let range = Area { + sheet: 0, + row: 2, + column: 2, + width: 1, + height: 1, + }; + + model + .update_range_style(&range, "alignment.vertical", "distributed") + .unwrap(); + let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment; + assert_eq!( + alignment, + Some(Alignment { + horizontal: HorizontalAlignment::General, + vertical: VerticalAlignment::Distributed, + wrap_text: false + }) + ); + + model + .update_range_style(&range, "alignment.vertical", "justify") + .unwrap(); + let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment; + assert_eq!( + alignment, + Some(Alignment { + horizontal: HorizontalAlignment::General, + vertical: VerticalAlignment::Justify, + wrap_text: false + }) + ); + + model.update_range_style(&range, "alignment", "").unwrap(); + let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment; + assert_eq!(alignment, None); + + model.undo().unwrap(); + + let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment; + assert_eq!( + alignment, + Some(Alignment { + horizontal: HorizontalAlignment::General, + vertical: VerticalAlignment::Justify, + wrap_text: false + }) + ); +} + +#[test] +fn alignment_errors() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + + assert_eq!( + model.update_range_style(&range, "alignment", "some"), + Err("Alignment must be empty, but found: 'some'.".to_string()) + ); + + assert_eq!( + model.update_range_style(&range, "alignment.vertical", "justified"), + Err("Invalid value for vertical alignment: 'justified'.".to_string()) + ); + + assert_eq!( + model.update_range_style(&range, "alignment.horizontal", "unjustified"), + Err("Invalid value for horizontal alignment: 'unjustified'.".to_string()) + ); + + model + .update_range_style(&range, "alignment.vertical", "justify") + .unwrap(); + + // Also fail if there is an alignment + + assert_eq!( + model.update_range_style(&range, "alignment", "some"), + Err("Alignment must be empty, but found: 'some'.".to_string()) + ); + + assert_eq!( + model.update_range_style(&range, "alignment.vertical", "justified"), + Err("Invalid value for vertical alignment: 'justified'.".to_string()) + ); + + assert_eq!( + model.update_range_style(&range, "alignment.horizontal", "unjustified"), + Err("Invalid value for horizontal alignment: 'unjustified'.".to_string()) + ); +} + +#[test] +fn basic_wrap_text() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + assert_eq!( + model.update_range_style(&range, "alignment.wrap_text", "T"), + Err("Invalid value for boolean: 'T'.".to_string()) + ); + model + .update_range_style(&range, "alignment.wrap_text", "true") + .unwrap(); + let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment; + assert_eq!( + alignment, + Some(Alignment { + horizontal: HorizontalAlignment::General, + vertical: VerticalAlignment::Bottom, + wrap_text: true + }) + ); + model.undo().unwrap(); + let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment; + assert_eq!(alignment, None); + + model.redo().unwrap(); + + let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment; + assert_eq!( + alignment, + Some(Alignment { + horizontal: HorizontalAlignment::General, + vertical: VerticalAlignment::Bottom, + wrap_text: true + }) + ); + + assert_eq!( + model.update_range_style(&range, "alignment.wrap_text", "True"), + Err("Invalid value for boolean: 'True'.".to_string()) + ); +} + +#[test] +fn more_basic_borders() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + + model + .update_range_style(&range, "border.left", "thick,#F1F1F1") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::Thick, + color: Some("#F1F1F1".to_owned()), + }) + ); + + model + .update_range_style(&range, "border.left", "slantDashDot,#F1F1F1") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::SlantDashDot, + color: Some("#F1F1F1".to_owned()), + }) + ); + + model + .update_range_style(&range, "border.left", "mediumDashDot,#F1F1F1") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::MediumDashDot, + color: Some("#F1F1F1".to_owned()), + }) + ); + + model + .update_range_style(&range, "border.left", "mediumDashDotDot,#F1F1F1") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::MediumDashDotDot, + color: Some("#F1F1F1".to_owned()), + }) + ); + + model + .update_range_style(&range, "border.left", "mediumDashed,#F1F1F1") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::MediumDashed, + color: Some("#F1F1F1".to_owned()), + }) + ); +} + +#[test] +fn border_errors() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + + assert_eq!( + model.update_range_style(&range, "border.lef", "thick,#F1F1F1"), + Err("Invalid style path: 'border.lef'.".to_string()) + ); + + assert_eq!( + model.update_range_style(&range, "border.left", "thic,#F1F1F1"), + Err("Invalid border style: 'thic'.".to_string()) + ); + + assert_eq!( + model.update_range_style(&range, "border.left", "thick,#F1F1F"), + Err("Invalid color: '#F1F1F'.".to_string()) + ); + + assert_eq!( + model.update_range_style(&range, "border.left", " "), + Err("Invalid border value: ' '.".to_string()) + ); + + assert_eq!( + model.update_range_style(&range, "border.left", "thick,#F1F1F1,thin"), + Err("Invalid border value: 'thick,#F1F1F1,thin'.".to_string()) + ); +} + +#[test] +fn empty_removes_border() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + model + .update_range_style(&range, "border.left", "mediumDashDotDot,#F1F1F1") + .unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!( + style.border.left, + Some(BorderItem { + style: BorderStyle::MediumDashDotDot, + color: Some("#F1F1F1".to_owned()), + }) + ); + + model.update_range_style(&range, "border.left", "").unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert_eq!(style.border.left, None); +} + +#[test] +fn false_removes_value() { + let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); + let range = Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + + // bold + model.update_range_style(&range, "font.b", "true").unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(style.font.b); + + model.update_range_style(&range, "font.b", "false").unwrap(); + let style = model.get_cell_style(0, 1, 1).unwrap(); + assert!(!style.font.b); +} diff --git a/base/src/test/user_model/test_undo_redo.rs b/base/src/test/user_model/test_undo_redo.rs new file mode 100644 index 0000000..67b5de6 --- /dev/null +++ b/base/src/test/user_model/test_undo_redo.rs @@ -0,0 +1,66 @@ +#![allow(clippy::unwrap_used)] + +use crate::{test::util::new_empty_model, UserModel}; + +#[test] +fn simple_undo_redo() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // at the beginning I cannot undo or redo + assert!(!model.can_undo()); + assert!(!model.can_redo()); + assert!(model.set_user_input(0, 1, 1, "=1+2").is_ok()); + + // Once I enter a value I can undo but not redo + assert!(model.can_undo()); + assert!(!model.can_redo()); + assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("3".to_string())); + + // If I undo, I can't undo anymore, but I can redo + assert!(model.undo().is_ok()); + assert!(!model.can_undo()); + assert!(model.can_redo()); + assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string())); + + // If I redo, I have the old value and formula + assert!(model.redo().is_ok()); + assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("3".to_string())); + assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+2".to_string())); + assert!(model.can_undo()); + assert!(!model.can_redo()); +} + +#[test] +fn undo_redo_respect_styles() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + assert!(model.set_user_input(0, 1, 1, "100").is_ok()); + assert!(model.set_user_input(0, 1, 1, "125$").is_ok()); + // The content of the cell is just the number 125 + assert_eq!(model.get_cell_content(0, 1, 1), Ok("125".to_string())); + assert!(model.undo().is_ok()); + // The cell has no currency number formatting + assert_eq!( + model.get_formatted_cell_value(0, 1, 1), + Ok("100".to_string()) + ); + assert_eq!(model.get_cell_content(0, 1, 1), Ok("100".to_string())); + assert!(model.redo().is_ok()); + // The cell has the number 125 formatted as '125$' + assert_eq!( + model.get_formatted_cell_value(0, 1, 1), + Ok("125$".to_string()) + ); + assert_eq!(model.get_cell_content(0, 1, 1), Ok("125".to_string())); +} + +#[test] +fn can_undo_can_redo() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + assert!(!model.can_undo()); + assert!(!model.can_redo()); + + assert!(model.undo().is_ok()); + assert!(model.redo().is_ok()); +} diff --git a/base/src/test/util.rs b/base/src/test/util.rs index d31f8cb..6f818a7 100644 --- a/base/src/test/util.rs +++ b/base/src/test/util.rs @@ -32,11 +32,11 @@ impl Model { let cell_reference = self._parse_reference(cell); let column = cell_reference.column; let row = cell_reference.row; - self.cell_formula(cell_reference.sheet, row, column) + self.get_cell_formula(cell_reference.sheet, row, column) .unwrap() } pub fn _get_text_at(&self, sheet: u32, row: i32, column: i32) -> String { - self.formatted_cell_value(sheet, row, column).unwrap() + self.get_formatted_cell_value(sheet, row, column).unwrap() } pub fn _get_text(&self, cell: &str) -> String { let CellReferenceIndex { sheet, row, column } = self._parse_reference(cell); diff --git a/base/src/user_model.rs b/base/src/user_model.rs new file mode 100644 index 0000000..15e1983 --- /dev/null +++ b/base/src/user_model.rs @@ -0,0 +1,1183 @@ +#![deny(missing_docs)] + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{ + constants, + expressions::{ + types::Area, + utils::{is_valid_column_number, is_valid_row}, + }, + model::Model, + types::{ + Alignment, BorderItem, BorderStyle, Cell, Col, HorizontalAlignment, Row, SheetInfo, Style, + VerticalAlignment, + }, + utils::is_valid_hex_color, +}; + +#[derive(Clone, Serialize, Deserialize)] +struct RowData { + row: Option, + data: HashMap, +} + +#[derive(Clone, Serialize, Deserialize)] +struct ColumnData { + column: Option, + data: HashMap, +} + +#[derive(Clone, Serialize, Deserialize)] +enum Diff { + // Cell diffs + SetCellValue { + sheet: u32, + row: i32, + column: i32, + new_value: String, + old_value: Box>, + }, + CellClearContents { + sheet: u32, + row: i32, + column: i32, + old_value: Box>, + }, + CellClearAll { + sheet: u32, + row: i32, + column: i32, + old_value: Box>, + old_style: Box