Compare commits
1 Commits
feature/dy
...
varum-chan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7858f7aa9 |
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)?
|
||||
|
||||
@@ -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;
|
||||
|
||||
155
base/src/test/test_model_merge_cell_fns.rs
Normal file
155
base/src/test/test_model_merge_cell_fns.rs
Normal 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(()));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
BIN
xlsx/tests/Merged_cells.xlsx
Normal file
BIN
xlsx/tests/Merged_cells.xlsx
Normal file
Binary file not shown.
Binary file not shown.
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user