diff --git a/base/src/cell.rs b/base/src/cell.rs index 70299f1..d9f08e7 100644 --- a/base/src/cell.rs +++ b/base/src/cell.rs @@ -176,3 +176,18 @@ impl Cell { } } } + +// Implementing methods for MergedCells struct + +impl MergedCells { + pub fn is_cell_part_of_merged_cells(&self, row: i32, col: i32) -> bool { + // This is merge Mother cell so do not include this cell as part of Merged Cells + if row == self.0 && col == self.1 { + return false; + } + + let result: bool = (row >= self.0 && row <= self.2) && (col >= self.1 && col <= self.3); + + result + } +} diff --git a/base/src/model.rs b/base/src/model.rs index 422b068..60abc6e 100644 --- a/base/src/model.rs +++ b/base/src/model.rs @@ -15,7 +15,7 @@ use crate::{ }, token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary}, types::*, - utils::{self, is_valid_column_number, is_valid_row}, + utils::{self, is_valid_column_number, is_valid_row, parse_reference_a1}, }, formatter::{ format::{format_number, parse_formatted_number}, @@ -747,6 +747,29 @@ impl Model { self.workbook.worksheet(sheet)?.is_empty_cell(row, column) } + /// Returns 'true' if the cell belongs to any Merged cells + /// # Examples + /// + /// ```rust + /// # use ironcalc_base::Model; + /// # fn main() -> Result<(), Box> { + /// let mut model = Model::new_empty("model", "en", "UTC")?; + /// model.merge_cells(0, "A1:D5"); + /// assert_eq!(model.is_part_of_merged_cells(0, 1, 2)?, true); + /// # Ok(()) + /// # } + /// ``` + pub fn is_part_of_merged_cells( + &self, + sheet: u32, + row: i32, + column: i32, + ) -> Result { + self.workbook + .worksheet(sheet)? + .is_part_of_merged_cells(row, column) + } + pub(crate) fn evaluate_cell(&mut self, cell_reference: CellReferenceIndex) -> CalcResult { let row_data = match self.workbook.worksheets[cell_reference.sheet as usize] .sheet_data @@ -1225,6 +1248,14 @@ impl Model { column: i32, value: &str, ) -> Result<(), String> { + // Checking first whether cell we are updating is part of Merged cells + // if so returning with Err + if self.is_part_of_merged_cells(sheet, row, column)? { + return Err(format!( + "Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible", + row, column + )); + } let style_index = self.get_cell_style_index(sheet, row, column)?; let new_style_index; if common::value_needs_quoting(value, &self.language) { @@ -1275,6 +1306,15 @@ impl Model { column: i32, value: bool, ) -> Result<(), String> { + // Checking first whether cell we are updating is part of Merged cells + // if so returning with Err + if self.is_part_of_merged_cells(sheet, row, column)? { + return Err(format!( + "Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible", + row, column + )); + } + 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 @@ -1317,6 +1357,14 @@ impl Model { column: i32, value: f64, ) -> Result<(), String> { + // Checking first whether cell we are updating is part of Merged cells + // if so returning with Err + if self.is_part_of_merged_cells(sheet, row, column)? { + return Err(format!( + "Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible", + row, column + )); + } 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 @@ -1362,6 +1410,12 @@ impl Model { column: i32, formula: String, ) -> Result<(), String> { + if self.is_part_of_merged_cells(sheet, row, column)? { + return Err(format!( + "Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible", + row, column + )); + } let mut style_index = self.get_cell_style_index(sheet, row, column)?; if self.workbook.styles.style_is_quote_prefix(style_index) { style_index = self @@ -1414,6 +1468,14 @@ impl Model { column: i32, value: String, ) -> Result<(), String> { + // Checking first whether cell we are updating is part of Merged cells + // if so returning with Err + if self.is_part_of_merged_cells(sheet, row, column)? { + return Err(format!( + "Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible", + row, column + )); + } // 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('\'') { @@ -1928,6 +1990,7 @@ impl Model { /// Sets the number of frozen rows to `frozen_rows` in the workbook. /// Fails if `frozen`_rows` is either too small (<0) or too large (>LAST_ROW)` pub fn set_frozen_rows(&mut self, sheet: u32, frozen_rows: i32) -> Result<(), String> { + // TODO: What is frozen rows and do we need to take of this if row we are frozing is part of merge cells ? if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) { if frozen_rows < 0 { return Err("Frozen rows cannot be negative".to_string()); @@ -1945,6 +2008,7 @@ impl Model { /// Sets the number of frozen columns to `frozen_column` in the workbook. /// Fails if `frozen`_columns` is either too small (<0) or too large (>LAST_COLUMN)` pub fn set_frozen_columns(&mut self, sheet: u32, frozen_columns: i32) -> Result<(), String> { + // TODO: What is frozen columns and do we need to take of this if column we are frozing is part of merge cells ? if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) { if frozen_columns < 0 { return Err("Frozen columns cannot be negative".to_string()); @@ -1986,6 +2050,164 @@ impl Model { .worksheet_mut(sheet)? .set_row_height(column, height) } + + fn parse_merged_range(&mut self, range: &str) -> Result<(i32, i32, i32, i32), String> { + let parts: Vec<&str> = range.split(':').collect(); + if parts.len() == 1 { + Err(format!("Invalid range: '{}'", range)) + } else if parts.len() == 2 { + match (parse_reference_a1(parts[0]), parse_reference_a1(parts[1])) { + (Some(left), Some(right)) => { + return Ok((left.row, left.column, right.row, right.column)); + } + _ => return Err(format!("Invalid range: '{}'", range)), + } + } else { + return Err(format!("Invalid range: '{}'", range)); + } + } + + // Implementing public APIS related to Merge cells handling + + /// Merges given selected cells + /// If no overlap, it will create that merged cells with left most top cell value representing the whole merged cells + /// If new merge cells creation overlaps with any of the existing merged cells, Overlapped merged cells gets unmerged + /// and new merge cells gets added + /// + /// # Examples + /// + /// ```rust + /// # use ironcalc_base::Model; + /// # use ironcalc_base::cell::CellValue; + /// # fn main() -> Result<(), Box> { + /// let mut model = Model::new_empty("model", "en", "UTC")?; + /// model.merge_cells(0, "D4:F6").unwrap(); + /// model.merge_cells(0, "A1:B4").unwrap(); + /// assert_eq!(model.workbook.worksheet(0).unwrap().merged_cells_list.len(), 2); + /// # Ok(()) + /// # } + /// ``` + /// + /// See also: + /// * [Model::update_cell_with_formula()] + /// * [Model::update_cell_with_number()] + /// * [Model::update_cell_with_bool()] + /// * [Model::update_cell_with_text()] + pub fn merge_cells(&mut self, sheet: u32, range_ref: &str) -> Result<(), String> { + match self.parse_merged_range(range_ref) { + Ok(parsed_merge_cell_range) => { + // ATTENTION 2: Below thing we can support here but keeping it simple + // Web or different client needs to keep this in mind + // User can give errored parse ranges like C3:A1 + // Where col_start and row_start and is greated then col_end and row_end + // Return error in these scenario + if parsed_merge_cell_range.0 > parsed_merge_cell_range.2 + || parsed_merge_cell_range.1 > parsed_merge_cell_range.3 + { + return Err( + "Invalid parse range. Merge Mother cell always be top left cell" + .to_string(), + ); + } + + let mut merged_cells_overlaped_list: Vec = Vec::new(); + // checking whether our new range overlaps with any of the already existing merged cells + // if so, need to unmerge those and create this new one + { + let worksheet = self.workbook.worksheet(sheet)?; + let merged_cells = worksheet.get_merged_cells_list(); + + for merge_node in merged_cells { + // checking whether any overlapping exist with this merge cell + if !(parsed_merge_cell_range.1 > merge_node.3 + || parsed_merge_cell_range.3 < merge_node.1 + || parsed_merge_cell_range.0 > merge_node.2 + || parsed_merge_cell_range.2 < merge_node.0) + { + // overlap has happened + merged_cells_overlaped_list.push(true); + } else { + merged_cells_overlaped_list.push(false); + } + } + } + + if !merged_cells_overlaped_list.is_empty() { + // Lets take Mutable ref to Merge cell and deletes all those nodes which has overlapped + let worksheet = self.workbook.worksheet_mut(sheet)?; + let merged_cells_list_mut = worksheet.get_merged_cells_list_mut(); + let mut merged_cells_overlaped_list_iter = merged_cells_overlaped_list.iter(); + merged_cells_list_mut + .retain(|_| !(*merged_cells_overlaped_list_iter.next().unwrap())) + } + + // Now need to update (n*m - 1) cells with empty cell ( except the Mother cell ) + for row_index in parsed_merge_cell_range.0..=parsed_merge_cell_range.2 { + for col_index in parsed_merge_cell_range.1..=parsed_merge_cell_range.3 { + // skip Mother cell + if row_index == parsed_merge_cell_range.0 + && col_index == parsed_merge_cell_range.2 + { + continue; + } + + //update the node with empty cell + { + self.workbook.worksheet_mut(sheet)?.update_cell( + row_index, + col_index, + Cell::EmptyCell { s: 0 }, + )?; + } + } + } + + let new_merged_cells = MergedCells::new(parsed_merge_cell_range); + { + self.workbook + .worksheet_mut(sheet)? + .merged_cells_list + .push(new_merged_cells); + } + } + Err(err) => { + return Err(err); + } + } + Ok(()) + } + + /// Unmerges a given/selected merged cells + /// Once unmerged, only top most left corner value gets retained and all the others will have empty cell + /// # Examples + /// + /// ```rust + /// # use ironcalc_base::Model; + /// # use ironcalc_base::cell::CellValue; + /// # fn main() -> Result<(), Box> { + /// let mut model = Model::new_empty("model", "en", "UTC")?; + /// model.merge_cells(0, "D4:F6"); + /// model.unmerge_cells(0, "D4:F6"); + /// # Ok(()) + /// # } + /// ``` + pub fn unmerge_cells(&mut self, sheet: u32, range_ref: &str) -> Result<(), String> { + let worksheet = self.workbook.worksheet(sheet)?; + let merged_cells = worksheet.get_merged_cells_list(); + for (index, merge_node) in merged_cells.iter().enumerate() { + let merge_block_range_ref = merge_node.get_merged_cells_str_ref()?; + // finding the merge cell node to be deleted + if merge_block_range_ref.as_str() == range_ref { + // Merge cell to be deleted is found + self.workbook + .worksheet_mut(sheet)? + .merged_cells_list + .remove(index); + return Ok(()); + } + } + Err("Invalid merge_cell_ref, Merged cells to be deleted is not found".to_string()) + } } #[cfg(test)] diff --git a/base/src/new_empty.rs b/base/src/new_empty.rs index a3da916..622b38f 100644 --- a/base/src/new_empty.rs +++ b/base/src/new_empty.rs @@ -57,7 +57,7 @@ impl Model { rows: vec![], comments: vec![], dimension: "A1".to_string(), - merge_cells: vec![], + merged_cells_list: vec![], name: name.to_string(), shared_formulas: vec![], sheet_data: Default::default(), diff --git a/base/src/styles.rs b/base/src/styles.rs index 4d41cc2..b80e0ff 100644 --- a/base/src/styles.rs +++ b/base/src/styles.rs @@ -223,6 +223,14 @@ impl Model { column: i32, style: &Style, ) -> Result<(), String> { + // Checking first whether cell we are updating is part of Merged cells + // if so returning with Err + if self.is_part_of_merged_cells(sheet, row, column)? { + return Err(format!( + "Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible", + row, column + )); + } let style_index = self.workbook.styles.get_style_index_or_create(style); self.workbook .worksheet_mut(sheet)? @@ -252,6 +260,14 @@ impl Model { column: i32, style_name: &str, ) -> Result<(), String> { + // Checking first whether cell we are updating is part of Merged cells + // if so returning with Err + if self.is_part_of_merged_cells(sheet, row, column)? { + return Err(format!( + "Cell row : {}, col : {} is part of merged cells block, so singular update to the cell is not possible", + row, column + )); + } let style_index = self.workbook.styles.get_style_index_by_name(style_name)?; self.workbook .worksheet_mut(sheet)? diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 222fe1b..d18d203 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -53,6 +53,7 @@ mod test_extend; mod test_fn_type; mod test_frozen_rows_and_columns; mod test_get_cell_content; +mod test_model_merge_cell_fns; mod test_percentage; mod test_set_functions_error_handling; mod test_today; diff --git a/base/src/test/test_model_merge_cell_fns.rs b/base/src/test/test_model_merge_cell_fns.rs new file mode 100644 index 0000000..77620ac --- /dev/null +++ b/base/src/test/test_model_merge_cell_fns.rs @@ -0,0 +1,155 @@ +#![allow(clippy::unwrap_used)] + +use crate::{test::util::new_empty_model, types::CellType}; + +#[test] +fn test_model_set_fns_related_to_merge_cells() { + let mut model = new_empty_model(); + + // creating a merge cell of D1:F2 + model.merge_cells(0, "D1:F2").unwrap(); + + // Updating the mother cell of Merge cells and expecting the update to go through + model.set_user_input(0, 1, 4, "Hello".to_string()).unwrap(); + assert_eq!(model.get_cell_content(0, 1, 4).unwrap(), "Hello"); + assert_eq!(model.get_cell_type(0, 1, 4).unwrap(), CellType::Text); + + // Updating cell which is not in Merge cell block + assert_eq!(model.set_user_input(0, 1, 3, "Hello".to_string()), Ok(())); + assert_eq!(model.get_cell_content(0, 1, 3), Ok("Hello".to_string())); + assert_eq!(model.get_cell_type(0, 1, 3), Ok(CellType::Text)); + + // 1: testing with set_user_input() + assert_eq!( + model + .set_user_input(0, 1, 5, "Hello".to_string()), + Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string()) + ); + assert_eq!(model.get_cell_content(0, 1, 5), Ok("".to_string())); + assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number)); + + // 2: testing with update_cell_with_bool() + assert_eq!( + model + .update_cell_with_bool(0, 1, 5, true), + Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string()) + ); + assert_eq!(model.get_cell_content(0, 1, 5), Ok("".to_string())); + assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number)); + + // 3: testing with update_cell_with_formula() + assert_eq!( + model + .update_cell_with_formula(0, 1, 5, "=SUM(A1+A2)".to_string()), + Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string()) + ); + assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number)); + + // 4: testing with update_cell_with_number() + assert_eq!( + model + .update_cell_with_number(0, 1, 5, 10.0), + Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string()) + ); + assert_eq!(model.get_cell_content(0, 1, 5), Ok("".to_string())); + assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number)); + + // 5: testing with update_cell_with_text() + assert_eq!( + model + .update_cell_with_text(0, 1, 5, "new text"), + Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string()) + ); + assert_eq!(model.get_cell_content(0, 1, 5), Ok("".to_string())); + assert_eq!(model.get_cell_type(0, 1, 5), Ok(CellType::Number)); +} + +#[test] +fn test_model_merge_cells_crud_api() { + let mut model = new_empty_model(); + + // creating a merge cell of D4:F6 + model.merge_cells(0, "D4:F6").unwrap(); + model + .set_user_input(0, 4, 4, "Merge Block".to_string()) + .unwrap(); + // CRUD APIS testing on Merge Cells + + // Case1: Creating a new merge cell without overlapping + // Newly created Merge block is left to D4:F6 + assert_eq!(model.merge_cells(0, "A1:B4"), Ok(())); + assert_eq!( + model.workbook.worksheet(0).unwrap().merged_cells_list.len(), + 2 + ); + model.set_user_input(0, 1, 1, "left".to_string()).unwrap(); + + // Newly created Merge block is right to D4:F6 + assert_eq!(model.merge_cells(0, "G1:H7"), Ok(())); + assert_eq!( + model.workbook.worksheet(0).unwrap().merged_cells_list.len(), + 3 + ); + model.set_user_input(0, 1, 7, "right".to_string()).unwrap(); + + // Newly created Merge block is above to D4:F6 + assert_eq!(model.merge_cells(0, "C1:D3"), Ok(())); + assert_eq!( + model.workbook.worksheet(0).unwrap().merged_cells_list.len(), + 4 + ); + model.set_user_input(0, 1, 3, "top".to_string()).unwrap(); + + // Newly created Merge block is down to D4:F6 + assert_eq!(model.merge_cells(0, "D8:E9"), Ok(())); + assert_eq!( + model.workbook.worksheet(0).unwrap().merged_cells_list.len(), + 5 + ); + model.set_user_input(0, 8, 4, "down".to_string()).unwrap(); + + // Case2: Creating a new merge cell with overlapping with other 3 merged cell + assert_eq!(model.merge_cells(0, "C1:G4"), Ok(())); + assert_eq!( + model.workbook.worksheet(0).unwrap().merged_cells_list.len(), + 3 + ); + model + .set_user_input(0, 1, 3, "overlapped_new_merge_block".to_string()) + .unwrap(); + + // Case3: Giving wrong parsing range + assert_eq!( + model.merge_cells(0, "C3:A1"), + Err("Invalid parse range. Merge Mother cell always be top left cell".to_string()) + ); + assert_eq!( + model.merge_cells(0, "CA:A1"), + Err("Invalid range: 'CA:A1'".to_string()) + ); + assert_eq!( + model.merge_cells(0, "C0:A1"), + Err("Invalid range: 'C0:A1'".to_string()) + ); + assert_eq!( + model.merge_cells(0, "C1:A0"), + Err("Invalid range: 'C1:A0'".to_string()) + ); + assert_eq!( + model.merge_cells(0, "C1"), + Err("Invalid range: 'C1'".to_string()) + ); + assert_eq!( + model.merge_cells(0, "C1:A1:B1"), + Err("Invalid range: 'C1:A1:B1'".to_string()) + ); + + // Case3: Giving wrong merge_ref, which would resulting in error (Merge cell to be deleted is not found) + assert_eq!( + model.unmerge_cells(0, "C1:E1"), + Err("Invalid merge_cell_ref, Merged cells to be deleted is not found".to_string()) + ); + + // Case4: unmerge scenario + assert_eq!(model.unmerge_cells(0, "C1:G4"), Ok(())); +} diff --git a/base/src/test/test_styles.rs b/base/src/test/test_styles.rs index b789d2f..3211137 100644 --- a/base/src/test/test_styles.rs +++ b/base/src/test/test_styles.rs @@ -62,3 +62,45 @@ fn test_create_named_style() { let style = model.get_style_for_cell(0, 1, 1).unwrap(); assert!(style.font.b); } + +#[test] +fn test_model_style_set_fns_in_merge_cell_context() { + let mut model = new_empty_model(); + + // creating a merge cell of D1:F2 + model.merge_cells(0, "D1:F2").unwrap(); + model.set_user_input(0, 1, 4, "Hello".to_string()).unwrap(); + + let mut style = model.get_style_for_cell(0, 1, 1).unwrap(); + assert!(!style.font.b); + style.font.b = true; + + // Updating the mother cell of Merge cells and expecting the update to go through + // This should make the text "Hello" in bold format + assert_eq!(model.set_cell_style(0, 1, 4, &style), Ok(())); + + // 1: testing with set_cell_style() + let original_style: Style = model.get_style_for_cell(0, 1, 5).unwrap(); + assert_eq!( + model + .set_cell_style(0, 1, 5, &style), + Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string()) + ); + assert_eq!(model.get_style_for_cell(0, 1, 5), Ok(original_style)); + + // 2: testing with set_cell_style_by_name + let mut style = model.get_style_for_cell(0, 1, 4).unwrap(); + style.font.b = true; + assert_eq!( + model.workbook.styles.create_named_style("bold", &style), + Ok(()) + ); + + let original_style: Style = model.get_style_for_cell(0, 1, 5).unwrap(); + assert_eq!( + model + .set_cell_style_by_name(0, 1, 5, "bold"), + Err("Cell row : 1, col : 5 is part of merged cells block, so singular update to the cell is not possible".to_string()) + ); + assert_eq!(model.get_style_for_cell(0, 1, 5), Ok(original_style)); +} diff --git a/base/src/test/test_worksheet.rs b/base/src/test/test_worksheet.rs index 587a7de..7147439 100644 --- a/base/src/test/test_worksheet.rs +++ b/base/src/test/test_worksheet.rs @@ -283,3 +283,87 @@ fn test_worksheet_navigate_to_edge_in_direction() { assert_eq!(navigate(8, 3, NavigationDirection::Up), (6, 3)); assert_eq!(navigate(9, 3, NavigationDirection::Up), (6, 3)); } + +// Tests Merge cells related functions of worksheet + +#[test] +fn test_merge_cell_fns_worksheet() { + let mut model = new_empty_model(); + + // Adding one Merge cell + model.merge_cells(0, "D1:E3").unwrap(); + + // Lets check whether D1 (Mother Merge cell) is part of Merge block or not + // It should not be considered as part of Merge cell + assert!(!model + .workbook + .worksheet(0) + .unwrap() + .is_part_of_merged_cells(1, 4) + .unwrap(),); + + // Lets give cell which is actually part of Merge block and expect true from fn + assert!(model + .workbook + .worksheet(0) + .unwrap() + .is_part_of_merged_cells(2, 4) + .unwrap()); + + // Lets give cell which is not a part of Merge block and expect false from fn + assert!(!model + .workbook + .worksheet(0) + .unwrap() + .is_part_of_merged_cells(2, 6) + .unwrap()); + + // Lets give an Invalid row + assert_eq!( + model + .workbook + .worksheet(0) + .unwrap() + .is_part_of_merged_cells(0, 1), + Err("Incorrect row or column".to_string()) + ); + + //Lets give Invalid column + assert_eq!( + model + .workbook + .worksheet(0) + .unwrap() + .is_part_of_merged_cells(1, 0), + Err("Incorrect row or column".to_string()) + ); + + // Verifying get fns of worksheet + assert_eq!( + model + .workbook + .worksheet(0) + .unwrap() + .get_merged_cells_list() + .len(), + 1 + ); + { + let merge_cell_vec = model + .workbook + .worksheet_mut(0) + .unwrap() + .get_merged_cells_list_mut(); + merge_cell_vec.remove(0); + + assert_eq!( + model + .workbook + .worksheet(0) + .unwrap() + .get_merged_cells_list() + .len(), + 0 + ); + } +} diff --git a/base/src/types.rs b/base/src/types.rs index c4f85fc..1852d68 100644 --- a/base/src/types.rs +++ b/base/src/types.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt::Display}; use crate::expressions::token::Error; +use crate::expressions::utils::number_to_column; fn default_as_false() -> bool { false @@ -110,7 +111,7 @@ pub struct Worksheet { pub sheet_id: u32, pub state: SheetState, pub color: Option, - pub merge_cells: Vec, + pub merged_cells_list: Vec, pub comments: Vec, pub frozen_rows: i32, pub frozen_columns: i32, @@ -351,6 +352,43 @@ pub enum FontScheme { None, } +// MergedCells type +// There will be one MergedCells struct maintained for every Merged cells that we load +// merge_cell_range : Its tuple having [row_start, column_start, row_end, column_end] +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct MergedCells(pub i32, pub i32, pub i32, pub i32); + +// implementing accessor function +impl MergedCells { + // Method which returns range_ref from the tuple + // ex : (3,1,4,2) is interpreted as A3:B4 + pub fn get_merged_cells_str_ref(&self) -> Result { + let start_column = number_to_column(self.1).ok_or(format!( + "Error while converting column start {} number to column string ref", + self.1 + ))?; + let end_column = number_to_column(self.3).ok_or(format!( + "Error while converting column end {} number to column string ref", + self.3 + ))?; + return Ok(start_column + + &self.0.to_string() + + &":".to_string() + + &end_column + + &self.2.to_string()); + } + + // Only Public function where Merge cell can be created + pub fn new(merge_cell_parsed_range: (i32, i32, i32, i32)) -> Self { + Self( + merge_cell_parsed_range.0, + merge_cell_parsed_range.1, + merge_cell_parsed_range.2, + merge_cell_parsed_range.3, + ) + } +} + impl Display for FontScheme { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { match self { diff --git a/base/src/worksheet.rs b/base/src/worksheet.rs index 027eafe..9fbc78c 100644 --- a/base/src/worksheet.rs +++ b/base/src/worksheet.rs @@ -524,6 +524,23 @@ impl Worksheet { Ok(is_empty) } + /// Returns true if cell part of Merged Cells. + /// First cell of Merged cells block is not considered as part of Merged cells + /// Ex : if Merged cells were A1-C3, A1 is not considered as part of Merged cells block + pub fn is_part_of_merged_cells(&self, row: i32, column: i32) -> Result { + if !is_valid_column_number(column) || !is_valid_row(row) { + return Err("Incorrect row or column".to_string()); + } + + // traverse through Vector of Merged Cells and return (linear search) + for merged_cells in &self.merged_cells_list { + if merged_cells.is_cell_part_of_merged_cells(row, column) { + return Ok(true); + } + } + Ok(false) + } + /// It provides convenient method for user navigation in the spreadsheet by jumping to edges. /// Spreadsheet engines usually allow this method of navigation by using CTRL+arrows. /// Behaviour summary: @@ -577,6 +594,16 @@ impl Worksheet { } } } + + /// Returns mutable reference to Vector of Merged cells list + pub fn get_merged_cells_list_mut(&mut self) -> &mut Vec { + &mut self.merged_cells_list + } + + /// Returns reference to Vector of Merged cells list + pub fn get_merged_cells_list(&self) -> &Vec { + &self.merged_cells_list + } } struct WalkFoundCells { diff --git a/xlsx/src/export/worksheets.rs b/xlsx/src/export/worksheets.rs index b3f2cbc..c594302 100644 --- a/xlsx/src/export/worksheets.rs +++ b/xlsx/src/export/worksheets.rs @@ -246,8 +246,18 @@ pub(crate) fn get_worksheet_xml( } let sheet_data = sheet_data_str.join(""); - for merge_cell_ref in &worksheet.merge_cells { - merged_cells_str.push(format!("")) + for merged_cells_ref in &worksheet.merged_cells_list { + let merged_cells_range_str_ref: String = match merged_cells_ref.get_merged_cells_str_ref() { + Ok(merged_cells_ref) => merged_cells_ref, + Err(err) => { + // ATTENTION : This should not happen. There should not be error while exporting + // already imported/created Mergedcells structure + // Currently, this function does not return any error. so logging the error and skipping this errored one + println!("{}", err); + continue; + } + }; + merged_cells_str.push(format!("")) } let merged_cells_count = merged_cells_str.len(); diff --git a/xlsx/src/import/worksheets.rs b/xlsx/src/import/worksheets.rs index 3d14b44..124ac4f 100644 --- a/xlsx/src/import/worksheets.rs +++ b/xlsx/src/import/worksheets.rs @@ -10,7 +10,7 @@ use ironcalc_base::{ utils::{column_to_number, parse_reference_a1}, }, types::{ - Cell, Col, Comment, DefinedName, Row, SheetData, SheetState, Table, Worksheet, + Cell, Col, Comment, DefinedName, MergedCells, Row, SheetData, SheetState, Table, Worksheet, WorksheetView, }, }; @@ -148,12 +148,12 @@ fn load_columns(ws: Node) -> Result, XlsxError> { Ok(cols) } -fn load_merge_cells(ws: Node) -> Result, XlsxError> { +fn load_merge_cells_nodes(ws: Node) -> Result, XlsxError> { // 18.3.1.55 Merge Cells // // // - let mut merge_cells = Vec::new(); + let mut merged_cells_list: Vec = Vec::new(); let merge_cells_nodes = ws .children() .filter(|n| n.has_tag_name("mergeCells")) @@ -161,10 +161,18 @@ fn load_merge_cells(ws: Node) -> Result, XlsxError> { if merge_cells_nodes.len() == 1 { for merge_cell in merge_cells_nodes[0].children() { let reference = get_attribute(&merge_cell, "ref")?.to_string(); - merge_cells.push(reference); + match parse_range(&reference) { + Ok(parsed_merge_cell_range) => { + let merge_cell_node = MergedCells::new(parsed_merge_cell_range); + merged_cells_list.push(merge_cell_node); + } + Err(err) => { + println!("encountered error while parsing merge cell ref : {}", err); + } + } } } - Ok(merge_cells) + Ok(merged_cells_list) } fn load_sheet_color(ws: Node) -> Result, XlsxError> { @@ -943,7 +951,7 @@ pub(super) fn load_sheet( sheet_data.insert(row_index, data_row); } - let merge_cells = load_merge_cells(ws)?; + let merge_cells_nodes = load_merge_cells_nodes(ws)?; // Conditional Formatting // @@ -982,7 +990,7 @@ pub(super) fn load_sheet( sheet_id, state: state.to_owned(), color, - merge_cells, + merged_cells_list: merge_cells_nodes, comments: settings.comments, frozen_rows: sheet_view.frozen_rows, frozen_columns: sheet_view.frozen_columns, diff --git a/xlsx/tests/Merged_cells.xlsx b/xlsx/tests/Merged_cells.xlsx new file mode 100644 index 0000000..146fd66 Binary files /dev/null and b/xlsx/tests/Merged_cells.xlsx differ diff --git a/xlsx/tests/example.ic b/xlsx/tests/example.ic index 740cd6b..930e4d6 100644 Binary files a/xlsx/tests/example.ic and b/xlsx/tests/example.ic differ diff --git a/xlsx/tests/test.rs b/xlsx/tests/test.rs index 0f24f1b..894e61e 100644 --- a/xlsx/tests/test.rs +++ b/xlsx/tests/test.rs @@ -406,7 +406,7 @@ fn test_exporting_merged_cells() { .worksheets .first() .unwrap() - .merge_cells + .merged_cells_list .clone(); // exporting and saving it in another xlsx model.evaluate(); @@ -423,7 +423,7 @@ fn test_exporting_merged_cells() { .worksheets .first() .unwrap() - .merge_cells + .merged_cells_list .clone(); assert_eq!(expected_merge_cell_ref, *got_merge_cell_ref); fs::remove_file(temp_file_name).unwrap(); @@ -437,7 +437,7 @@ fn test_exporting_merged_cells() { .worksheets .get_mut(0) .unwrap() - .merge_cells + .merged_cells_list .clear(); save_to_xlsx(&temp_model, temp_file_name).unwrap(); @@ -447,7 +447,7 @@ fn test_exporting_merged_cells() { .worksheets .first() .unwrap() - .merge_cells + .merged_cells_list .len(); assert!(*got_merge_cell_ref_cnt == 0); } @@ -494,3 +494,73 @@ fn test_documentation_xlsx() { } fs::remove_dir_all(&dir).unwrap(); } + +#[test] +fn test_merge_cell_import_export_behaviors() { + // loading the xlsx file containing merged cells + let example_file_name = "tests/Merged_cells.xlsx"; + let mut model = load_from_xlsx(example_file_name, "en", "UTC").unwrap(); + + // Case1 : To check whether Merge cells structures got imported properly or not + let imported_merge_cell_vec = model.workbook.worksheet(0).unwrap().get_merged_cells_list(); + + assert_eq!(imported_merge_cell_vec.len(), 5); + let range_refs_of_merge_cell: Vec = imported_merge_cell_vec + .iter() + .map(|cell| cell.get_merged_cells_str_ref().unwrap()) + .collect(); + assert_eq!( + range_refs_of_merge_cell, + [ + "C1:D3".to_string(), + "A1:B4".to_string(), + "G1:H7".to_string(), + "D8:E9".to_string(), + "D4:F6".to_string() + ] + ); + + // Create one More Merge cell which Overlaps with 3 More + model.merge_cells(0, "A1:D5").unwrap(); + model + .set_user_input(0, 1, 1, "New overlapped Merge cell".to_string()) + .unwrap(); + + let mut style = model.get_style_for_cell(0, 1, 1).unwrap(); + style.font.b = true; + assert_eq!( + model.workbook.styles.create_named_style("bold", &style), + Ok(()) + ); + + model.set_cell_style_by_name(0, 1, 1, "bold").unwrap(); + + // Lets export to different Excell + let exported_merge_cell_xlsx = "temporary_exported_mergecells.xlsx"; + save_to_xlsx(&model, exported_merge_cell_xlsx).unwrap(); + + { + let temp_model = load_from_xlsx(exported_merge_cell_xlsx, "en", "UTC").unwrap(); + // Loading the exported sheet back and verifying whether it got exported properly or not + let imported_merge_cell_vec = temp_model + .workbook + .worksheet(0) + .unwrap() + .get_merged_cells_list(); + + assert_eq!(imported_merge_cell_vec.len(), 3); + let range_refs_of_merge_cell: Vec = imported_merge_cell_vec + .iter() + .map(|cell| cell.get_merged_cells_str_ref().unwrap()) + .collect(); + assert_eq!( + range_refs_of_merge_cell, + [ + "G1:H7".to_string(), + "D8:E9".to_string(), + "A1:D5".to_string() + ] + ); + } + fs::remove_file(exported_merge_cell_xlsx).unwrap(); +}