UPDATE: Dump of initial files
This commit is contained in:
630
base/src/worksheet.rs
Normal file
630
base/src/worksheet.rs
Normal file
@@ -0,0 +1,630 @@
|
||||
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)
|
||||
}
|
||||
|
||||
fn update_cell(&mut self, row: i32, column: i32, new_cell: Cell) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
} else {
|
||||
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 cols = &mut self.cols;
|
||||
let col = Col {
|
||||
min: column,
|
||||
max: column,
|
||||
width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR,
|
||||
custom_width: true,
|
||||
style: Some(style_index),
|
||||
};
|
||||
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.style = Some(style_index);
|
||||
return Ok(());
|
||||
} else {
|
||||
// We need to split the result
|
||||
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,
|
||||
};
|
||||
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(())
|
||||
}
|
||||
|
||||
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) {
|
||||
match self.cell_mut(row, column) {
|
||||
Some(cell) => {
|
||||
cell.set_style(style_index);
|
||||
}
|
||||
None => {
|
||||
self.set_cell_empty_with_style(row, column, style_index);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
let cell = Cell::new_error(error, style);
|
||||
self.update_cell(row, column, cell);
|
||||
}
|
||||
|
||||
pub fn set_cell_empty(&mut self, row: i32, column: i32) {
|
||||
let s = self.get_style(row, column);
|
||||
let cell = Cell::EmptyCell { s };
|
||||
self.update_cell(row, column, cell);
|
||||
}
|
||||
|
||||
pub fn set_cell_empty_with_style(&mut self, row: i32, column: i32, style: i32) {
|
||||
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());
|
||||
} else 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());
|
||||
} else 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> {
|
||||
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: None,
|
||||
};
|
||||
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(());
|
||||
} else {
|
||||
// We need to split the result
|
||||
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 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);
|
||||
} else {
|
||||
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),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user