adding merge cell logic processing

formatting commit

addressing testcase failures

adding one more scenario to case

adding one more scenario to case

Adding update and unmerge functions for merge cell handling

adding one more case to testcase

adding testcases to base code

adding testcase for import/export

adding documentation to some of the PUB function

fixing warnings and test warnings

adding merge cell part cell update restriction to public sytle set fns

addressing reviwers comment : Changed Mergedcell structure and its side effercts

reverting it back to non pub.

renaming update_merge_cells to just merge_cells in model

renaming *unmerge_merged_cell* to *unmerge_cells*

addressing other reviewer's comment + cosmetica naming adjustments

cosmetic changes
This commit is contained in:
varuntumbe
2024-09-15 18:54:16 +05:30
committed by Nicolás Hatcher
parent 3270d587ac
commit e7858f7aa9
15 changed files with 704 additions and 16 deletions

View File

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

View File

@@ -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<dyn std::error::Error>> {
/// 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<bool, String> {
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<dyn std::error::Error>> {
/// 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<bool> = 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<dyn std::error::Error>> {
/// 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)]

View File

@@ -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(),

View File

@@ -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)?

View File

@@ -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;

View File

@@ -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(()));
}

View File

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

View File

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

View File

@@ -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<String>,
pub merge_cells: Vec<String>,
pub merged_cells_list: Vec<MergedCells>,
pub comments: Vec<Comment>,
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<String, String> {
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 {

View File

@@ -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<bool, String> {
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<MergedCells> {
&mut self.merged_cells_list
}
/// Returns reference to Vector of Merged cells list
pub fn get_merged_cells_list(&self) -> &Vec<MergedCells> {
&self.merged_cells_list
}
}
struct WalkFoundCells {

View File

@@ -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!("<mergeCell ref=\"{merge_cell_ref}\"/>"))
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!("<mergeCell ref=\"{merged_cells_range_str_ref}\"/>"))
}
let merged_cells_count = merged_cells_str.len();

View File

@@ -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<Vec<Col>, XlsxError> {
Ok(cols)
}
fn load_merge_cells(ws: Node) -> Result<Vec<String>, XlsxError> {
fn load_merge_cells_nodes(ws: Node) -> Result<Vec<MergedCells>, XlsxError> {
// 18.3.1.55 Merge Cells
// <mergeCells count="1">
// <mergeCell ref="K7:L10"/>
// </mergeCells>
let mut merge_cells = Vec::new();
let mut merged_cells_list: Vec<MergedCells> = 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<Vec<String>, 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<Option<String>, XlsxError> {
@@ -943,7 +951,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
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
// <conditionalFormatting sqref="B1:B9">
@@ -982,7 +990,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
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,

Binary file not shown.

Binary file not shown.

View File

@@ -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<String> = 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<String> = 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();
}