Files
IronCalc/base/src/worksheet.rs
Varun Hegde 2b03b3e3b9 Error Handling of public Set functions (#88)
What are we trying to achieve ?

++ Currently all the major public set functions is panic prone and does not handle and return error. This PR tries to address to all those functions.

What major errors that could happen in these functions ?

++ All the functions which are being made as error safe is being tested against invalid sheet, row and column values, which could given by user

What are the list of functions whose return type has been altered ?

**base/src/model.rs**
1. update_cell_with_text
2. update_cell_with_bool
3. update_cell_with_number
4. set_user_input
5. get_cell_style_index
6. get_style_for_cell
7. set_cell_with_string

++> New functions being added

1. set_cell_with_boolean
2. set_cell_with_number

**base/src/styles.rs**

1. get_style_with_quote_prefix
3. get_style_with_format
4. get_style_without_quote_prefix
5. get_style

**base/src/worksheet.rs**

1. update_cell
2. set_cell_style
3. set_cell_with_formula
4. set_cell_with_number
6. set_cell_with_string
8. set_cell_with_boolean
9. set_cell_with_error
10. cell_clear_contents
11. cell_clear_contents_with_style

++> Above is the comprehensive list of all functions being ( most are public, some are private ) altered for better error handling. As a side effect of changing function signature, there are many changes being done to other functions ( mostly adding "?" to enable to error propagation further )
2024-09-14 17:37:31 +02:00

635 lines
20 KiB
Rust

use crate::constants::{self, LAST_COLUMN, LAST_ROW};
use crate::expressions::types::CellReferenceIndex;
use crate::expressions::utils::{is_valid_column_number, is_valid_row};
use crate::{expressions::token::Error, types::*};
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq)]
pub struct WorksheetDimension {
pub min_row: i32,
pub max_row: i32,
pub min_column: i32,
pub max_column: i32,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum NavigationDirection {
Left,
Right,
Up,
Down,
}
impl Worksheet {
pub fn get_name(&self) -> String {
self.name.clone()
}
pub fn get_sheet_id(&self) -> u32 {
self.sheet_id
}
pub fn set_name(&mut self, name: &str) {
self.name = name.to_string();
}
pub fn cell(&self, row: i32, column: i32) -> Option<&Cell> {
self.sheet_data.get(&row)?.get(&column)
}
pub(crate) fn cell_mut(&mut self, row: i32, column: i32) -> Option<&mut Cell> {
self.sheet_data.get_mut(&row)?.get_mut(&column)
}
pub(crate) fn update_cell(
&mut self,
row: i32,
column: i32,
new_cell: Cell,
) -> Result<(), String> {
// validate row and column arg before updating cell of worksheet
if !is_valid_row(row) || !is_valid_column_number(column) {
return Err("Incorrect row or column".to_string());
}
match self.sheet_data.get_mut(&row) {
Some(column_data) => match column_data.get(&column) {
Some(_cell) => {
column_data.insert(column, new_cell);
}
None => {
column_data.insert(column, new_cell);
}
},
None => {
let mut column_data = HashMap::new();
column_data.insert(column, new_cell);
self.sheet_data.insert(row, column_data);
}
}
Ok(())
}
// TODO [MVP]: Pass the cell style from the model
// See: get_style_for_cell
fn get_row_column_style(&self, row_index: i32, column_index: i32) -> i32 {
let rows = &self.rows;
for row in rows {
if row.r == row_index {
if row.custom_format {
return row.s;
}
break;
}
}
let cols = &self.cols;
for column in cols.iter() {
let min = column.min;
let max = column.max;
if column_index >= min && column_index <= max {
return column.style.unwrap_or(0);
}
}
0
}
pub fn get_style(&self, row: i32, column: i32) -> i32 {
match self.sheet_data.get(&row) {
Some(column_data) => match column_data.get(&column) {
Some(cell) => cell.get_style(),
None => self.get_row_column_style(row, column),
},
None => self.get_row_column_style(row, column),
}
}
pub fn set_style(&mut self, style_index: i32) -> Result<(), String> {
self.cols = vec![Col {
min: 1,
max: constants::LAST_COLUMN,
width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR,
custom_width: true,
style: Some(style_index),
}];
Ok(())
}
pub fn set_column_style(&mut self, column: i32, style_index: i32) -> Result<(), String> {
let width = constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR;
self.set_column_width_and_style(column, width, Some(style_index))
}
pub fn set_row_style(&mut self, row: i32, style_index: i32) -> Result<(), String> {
for r in self.rows.iter_mut() {
if r.r == row {
r.s = style_index;
r.custom_format = true;
return Ok(());
}
}
self.rows.push(Row {
height: constants::DEFAULT_ROW_HEIGHT / constants::ROW_HEIGHT_FACTOR,
r: row,
custom_format: true,
custom_height: true,
s: style_index,
hidden: false,
});
Ok(())
}
pub fn set_cell_style(
&mut self,
row: i32,
column: i32,
style_index: i32,
) -> Result<(), String> {
match self.cell_mut(row, column) {
Some(cell) => {
cell.set_style(style_index);
}
None => {
self.cell_clear_contents_with_style(row, column, style_index)?;
}
}
Ok(())
// TODO: cleanup check if the old cell style is still in use
}
pub fn set_cell_with_formula(
&mut self,
row: i32,
column: i32,
index: i32,
style: i32,
) -> Result<(), String> {
let cell = Cell::new_formula(index, style);
self.update_cell(row, column, cell)
}
pub fn set_cell_with_number(
&mut self,
row: i32,
column: i32,
value: f64,
style: i32,
) -> Result<(), String> {
let cell = Cell::new_number(value, style);
self.update_cell(row, column, cell)
}
pub fn set_cell_with_string(
&mut self,
row: i32,
column: i32,
index: i32,
style: i32,
) -> Result<(), String> {
let cell = Cell::new_string(index, style);
self.update_cell(row, column, cell)
}
pub fn set_cell_with_boolean(
&mut self,
row: i32,
column: i32,
value: bool,
style: i32,
) -> Result<(), String> {
let cell = Cell::new_boolean(value, style);
self.update_cell(row, column, cell)
}
pub fn set_cell_with_error(
&mut self,
row: i32,
column: i32,
error: Error,
style: i32,
) -> Result<(), String> {
let cell = Cell::new_error(error, style);
self.update_cell(row, column, cell)
}
pub fn cell_clear_contents(&mut self, row: i32, column: i32) -> Result<(), String> {
let s = self.get_style(row, column);
let cell = Cell::EmptyCell { s };
self.update_cell(row, column, cell)
}
pub fn cell_clear_contents_with_style(
&mut self,
row: i32,
column: i32,
style: i32,
) -> Result<(), String> {
let cell = Cell::EmptyCell { s: style };
self.update_cell(row, column, cell)
}
pub fn set_frozen_rows(&mut self, frozen_rows: i32) -> Result<(), String> {
if frozen_rows < 0 {
return Err("Frozen rows cannot be negative".to_string());
}
if frozen_rows >= constants::LAST_ROW {
return Err("Too many rows".to_string());
}
self.frozen_rows = frozen_rows;
Ok(())
}
pub fn set_frozen_columns(&mut self, frozen_columns: i32) -> Result<(), String> {
if frozen_columns < 0 {
return Err("Frozen columns cannot be negative".to_string());
}
if frozen_columns >= constants::LAST_COLUMN {
return Err("Too many columns".to_string());
}
self.frozen_columns = frozen_columns;
Ok(())
}
/// Changes the height of a row.
/// * If the row does not a have a style we add it.
/// * If it has we modify the height and make sure it is applied.
/// Fails if column index is outside allowed range.
pub fn set_row_height(&mut self, row: i32, height: f64) -> Result<(), String> {
if !is_valid_row(row) {
return Err(format!("Row number '{row}' is not valid."));
}
let rows = &mut self.rows;
for r in rows.iter_mut() {
if r.r == row {
r.height = height / constants::ROW_HEIGHT_FACTOR;
r.custom_height = true;
return Ok(());
}
}
rows.push(Row {
height: height / constants::ROW_HEIGHT_FACTOR,
r: row,
custom_format: false,
custom_height: true,
s: 0,
hidden: false,
});
Ok(())
}
/// Changes the width of a column.
/// * If the column does not a have a width we simply add it
/// * If it has, it might be part of a range and we ned to split the range.
/// Fails if column index is outside allowed range.
pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> {
self.set_column_width_and_style(column, width, None)
}
pub(crate) fn set_column_width_and_style(
&mut self,
column: i32,
width: f64,
style: Option<i32>,
) -> Result<(), String> {
if !is_valid_column_number(column) {
return Err(format!("Column number '{column}' is not valid."));
}
let cols = &mut self.cols;
let mut col = Col {
min: column,
max: column,
width: width / constants::COLUMN_WIDTH_FACTOR,
custom_width: true,
style,
};
let mut index = 0;
let mut split = false;
for c in cols.iter_mut() {
let min = c.min;
let max = c.max;
if min <= column && column <= max {
if min == column && max == column {
c.width = width / constants::COLUMN_WIDTH_FACTOR;
return Ok(());
}
split = true;
break;
}
if column < min {
// We passed, we should insert at index
break;
}
index += 1;
}
if split {
let min = cols[index].min;
let max = cols[index].max;
let pre = Col {
min,
max: column - 1,
width: cols[index].width,
custom_width: cols[index].custom_width,
style: cols[index].style,
};
let post = Col {
min: column + 1,
max,
width: cols[index].width,
custom_width: cols[index].custom_width,
style: cols[index].style,
};
col.style = cols[index].style;
cols.remove(index);
if column != max {
cols.insert(index, post);
}
cols.insert(index, col);
if column != min {
cols.insert(index, pre);
}
} else {
cols.insert(index, col);
}
Ok(())
}
/// Return the width of a column in pixels
pub fn get_column_width(&self, column: i32) -> Result<f64, String> {
if !is_valid_column_number(column) {
return Err(format!("Column number '{column}' is not valid."));
}
let cols = &self.cols;
for col in cols {
let min = col.min;
let max = col.max;
if column >= min && column <= max {
if col.custom_width {
return Ok(col.width * constants::COLUMN_WIDTH_FACTOR);
}
break;
}
}
Ok(constants::DEFAULT_COLUMN_WIDTH)
}
// Returns non empty cells in a column
pub fn column_cell_references(&self, column: i32) -> Result<Vec<CellReferenceIndex>, String> {
let mut column_cell_references: Vec<CellReferenceIndex> = Vec::new();
if !is_valid_column_number(column) {
return Err(format!("Column number '{column}' is not valid."));
}
for row in self.sheet_data.keys() {
if self.cell(*row, column).is_some() {
column_cell_references.push(CellReferenceIndex {
sheet: self.sheet_id,
row: *row,
column,
});
}
}
Ok(column_cell_references)
}
/// Returns the height of a row in pixels
pub fn row_height(&self, row: i32) -> Result<f64, String> {
if !is_valid_row(row) {
return Err(format!("Row number '{row}' is not valid."));
}
let rows = &self.rows;
for r in rows {
if r.r == row {
return Ok(r.height * constants::ROW_HEIGHT_FACTOR);
}
}
Ok(constants::DEFAULT_ROW_HEIGHT)
}
/// Returns non empty cells in a row
pub fn row_cell_references(&self, row: i32) -> Result<Vec<CellReferenceIndex>, String> {
let mut row_cell_references: Vec<CellReferenceIndex> = Vec::new();
if !is_valid_row(row) {
return Err(format!("Row number '{row}' is not valid."));
}
for (row_index, columns) in self.sheet_data.iter() {
if *row_index == row {
for column in columns.keys() {
row_cell_references.push(CellReferenceIndex {
sheet: self.sheet_id,
row,
column: *column,
})
}
}
}
Ok(row_cell_references)
}
/// Returns non empty cells
pub fn cell_references(&self) -> Result<Vec<CellReferenceIndex>, String> {
let mut cell_references: Vec<CellReferenceIndex> = Vec::new();
for (row, columns) in self.sheet_data.iter() {
for column in columns.keys() {
cell_references.push(CellReferenceIndex {
sheet: self.sheet_id,
row: *row,
column: *column,
})
}
}
Ok(cell_references)
}
/// Calculates dimension of the sheet. This function isn't cheap to calculate.
pub fn dimension(&self) -> WorksheetDimension {
// FIXME: It's probably better to just track the size as operations happen.
if self.sheet_data.is_empty() {
return WorksheetDimension {
min_row: 1,
max_row: 1,
min_column: 1,
max_column: 1,
};
}
let mut row_range: Option<(i32, i32)> = None;
let mut column_range: Option<(i32, i32)> = None;
for (row_index, columns) in &self.sheet_data {
row_range = if let Some((current_min, current_max)) = row_range {
Some((current_min.min(*row_index), current_max.max(*row_index)))
} else {
Some((*row_index, *row_index))
};
for column_index in columns.keys() {
column_range = if let Some((current_min, current_max)) = column_range {
Some((
current_min.min(*column_index),
current_max.max(*column_index),
))
} else {
Some((*column_index, *column_index))
}
}
}
let dimension = if let Some((min_row, max_row)) = row_range {
if let Some((min_column, max_column)) = column_range {
Some(WorksheetDimension {
min_row,
min_column,
max_row,
max_column,
})
} else {
None
}
} else {
None
};
dimension.unwrap_or(WorksheetDimension {
min_row: 1,
max_row: 1,
min_column: 1,
max_column: 1,
})
}
/// Returns true if cell is completely empty.
/// Cell with formula that evaluates to empty string is not considered empty.
pub fn is_empty_cell(&self, row: i32, column: i32) -> Result<bool, String> {
if !is_valid_column_number(column) || !is_valid_row(row) {
return Err("Row or column is outside valid range.".to_string());
}
let is_empty = if let Some(data_row) = self.sheet_data.get(&row) {
if let Some(cell) = data_row.get(&column) {
matches!(cell, Cell::EmptyCell { .. })
} else {
true
}
} else {
true
};
Ok(is_empty)
}
/// 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:
/// - if starting cell is empty then find first non empty cell in given direction
/// - if starting cell is not empty, and neighbour in given direction is empty, then find
/// first non empty cell in given direction
/// - if starting cell is not empty, and neighbour in given direction is also not empty, then
/// find last non empty cell in given direction
pub fn navigate_to_edge_in_direction(
&self,
row: i32,
column: i32,
direction: NavigationDirection,
) -> Result<(i32, i32), String> {
if !is_valid_column_number(column) || !is_valid_row(row) {
return Err("Row or column is outside valid range.".to_string());
}
let start_cell = (row, column);
let neighbour_cell = if let Some(cell) = step_in_direction(start_cell, direction) {
cell
} else {
return Ok((start_cell.0, start_cell.1));
};
if self.is_empty_cell(start_cell.0, start_cell.1)? {
// Find first non-empty cell or move to the end.
let found_cells = walk_in_direction(start_cell, direction, |(row, column)| {
Ok(!self.is_empty_cell(row, column)?)
})?;
Ok(match found_cells.found_cell {
Some(cell) => cell,
None => found_cells.previous_cell,
})
} else {
// Neighbour cell is empty => find FIRST that is NOT empty
// Neighbour cell is not empty => find LAST that is NOT empty in sequence
if self.is_empty_cell(neighbour_cell.0, neighbour_cell.1)? {
let found_cells = walk_in_direction(start_cell, direction, |(row, column)| {
Ok(!self.is_empty_cell(row, column)?)
})?;
Ok(match found_cells.found_cell {
Some(cell) => cell,
None => found_cells.previous_cell,
})
} else {
let found_cells = walk_in_direction(start_cell, direction, |(row, column)| {
self.is_empty_cell(row, column)
})?;
Ok(found_cells.previous_cell)
}
}
}
}
struct WalkFoundCells {
/// If cell is found, it contains coordinates of the cell, otherwise None
found_cell: Option<(i32, i32)>,
/// Previous cell in chain relative to `found_cell`.
/// If `found_cell` is None then it's last considered cell.
previous_cell: (i32, i32),
}
/// Walks in direction until condition is met or boundary reached.
/// Returns tuple `(current_cell, previous_cell)`. `current_cell` is either None or passes predicate
fn walk_in_direction<F>(
start_cell: (i32, i32),
direction: NavigationDirection,
predicate: F,
) -> Result<WalkFoundCells, String>
where
F: Fn((i32, i32)) -> Result<bool, String>,
{
let mut previous_cell = start_cell;
let mut current_cell = step_in_direction(start_cell, direction);
while let Some(cell) = current_cell {
if !predicate((cell.0, cell.1))? {
previous_cell = cell;
current_cell = step_in_direction(cell, direction);
} else {
break;
}
}
Ok(WalkFoundCells {
found_cell: current_cell,
previous_cell,
})
}
/// Returns coordinate of cell in given direction from given cell.
/// Returns `None` if steps over the edge.
fn step_in_direction(
(row, column): (i32, i32),
direction: NavigationDirection,
) -> Option<(i32, i32)> {
if (row == 1 && direction == NavigationDirection::Up)
|| (row == LAST_ROW && direction == NavigationDirection::Down)
|| (column == 1 && direction == NavigationDirection::Left)
|| (column == LAST_COLUMN && direction == NavigationDirection::Right)
{
return None;
}
Some(match direction {
NavigationDirection::Left => (row, column - 1),
NavigationDirection::Right => (row, column + 1),
NavigationDirection::Up => (row - 1, column),
NavigationDirection::Down => (row + 1, column),
})
}