Files
IronCalc/base/src/user_model.rs
2024-05-07 22:52:47 +02:00

1280 lines
39 KiB
Rust

#![deny(missing_docs)]
use std::{collections::HashMap, fmt::Debug};
use bitcode::{Decode, Encode};
use crate::{
constants,
expressions::{
types::Area,
utils::{is_valid_column_number, is_valid_row},
},
model::Model,
types::{
Alignment, BorderItem, BorderStyle, Cell, CellType, Col, HorizontalAlignment, Row,
SheetProperties, Style, VerticalAlignment,
},
utils::is_valid_hex_color,
};
#[derive(Clone, Encode, Decode)]
struct RowData {
row: Option<Row>,
data: HashMap<i32, Cell>,
}
#[derive(Clone, Encode, Decode)]
struct ColumnData {
column: Option<Col>,
data: HashMap<i32, Cell>,
}
#[derive(Clone, Encode, Decode)]
enum Diff {
// Cell diffs
SetCellValue {
sheet: u32,
row: i32,
column: i32,
new_value: String,
old_value: Box<Option<Cell>>,
},
CellClearContents {
sheet: u32,
row: i32,
column: i32,
old_value: Box<Option<Cell>>,
},
CellClearAll {
sheet: u32,
row: i32,
column: i32,
old_value: Box<Option<Cell>>,
old_style: Box<Style>,
},
SetCellStyle {
sheet: u32,
row: i32,
column: i32,
old_value: Box<Style>,
new_value: Box<Style>,
},
// Column and Row diffs
SetColumnWidth {
sheet: u32,
column: i32,
new_value: f64,
old_value: f64,
},
SetRowHeight {
sheet: u32,
row: i32,
new_value: f64,
old_value: f64,
},
InsertRow {
sheet: u32,
row: i32,
},
DeleteRow {
sheet: u32,
row: i32,
old_data: Box<RowData>,
},
InsertColumn {
sheet: u32,
column: i32,
},
DeleteColumn {
sheet: u32,
column: i32,
old_data: Box<ColumnData>,
},
SetFrozenRowsCount {
sheet: u32,
new_value: i32,
old_value: i32,
},
SetFrozenColumnsCount {
sheet: u32,
new_value: i32,
old_value: i32,
},
DeleteSheet {
sheet: u32,
},
NewSheet {
index: u32,
name: String,
},
RenameSheet {
index: u32,
old_value: String,
new_value: String,
},
SetSheetColor {
index: u32,
old_value: String,
new_value: String,
},
}
type DiffList = Vec<Diff>;
#[derive(Default)]
struct History {
undo_stack: Vec<DiffList>,
redo_stack: Vec<DiffList>,
}
impl History {
fn push(&mut self, diff_list: DiffList) {
self.undo_stack.push(diff_list);
self.redo_stack = vec![];
}
fn undo(&mut self) -> Option<Vec<Diff>> {
match self.undo_stack.pop() {
Some(diff_list) => {
self.redo_stack.push(diff_list.clone());
Some(diff_list)
}
None => None,
}
}
fn redo(&mut self) -> Option<Vec<Diff>> {
match self.redo_stack.pop() {
Some(diff_list) => {
self.undo_stack.push(diff_list.clone());
Some(diff_list)
}
None => None,
}
}
fn clear(&mut self) {
self.redo_stack = vec![];
self.undo_stack = vec![];
}
}
#[derive(Clone, Encode, Decode)]
enum DiffType {
Undo,
Redo,
}
#[derive(Clone, Encode, Decode)]
struct QueueDiffs {
r#type: DiffType,
list: DiffList,
}
fn boolean(value: &str) -> Result<bool, String> {
match value {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(format!("Invalid value for boolean: '{value}'.")),
}
}
fn color(value: &str) -> Result<Option<String>, String> {
if value.is_empty() {
return Ok(None);
}
if !is_valid_hex_color(value) {
return Err(format!("Invalid color: '{value}'."));
}
Ok(Some(value.to_owned()))
}
fn border(value: &str) -> Result<Option<BorderItem>, String> {
if value.is_empty() {
return Ok(None);
}
let parts = value.split(',');
let values = parts.collect::<Vec<&str>>();
match values[..] {
[border_style, color_str] => {
let style = match border_style {
"thin" => BorderStyle::Thin,
"medium" => BorderStyle::Medium,
"thick" => BorderStyle::Thick,
"double" => BorderStyle::Double,
"dotted" => BorderStyle::Dotted,
"slantDashDot" => BorderStyle::SlantDashDot,
"mediumDashed" => BorderStyle::MediumDashed,
"mediumDashDotDot" => BorderStyle::MediumDashDotDot,
"mediumDashDot" => BorderStyle::MediumDashDot,
_ => {
return Err(format!("Invalid border style: '{border_style}'."));
}
};
Ok(Some(BorderItem {
style,
color: color(color_str)?,
}))
}
_ => Err(format!("Invalid border value: '{value}'.")),
}
}
fn horizontal(value: &str) -> Result<HorizontalAlignment, String> {
match value {
"center" => Ok(HorizontalAlignment::Center),
"centerContinuous" => Ok(HorizontalAlignment::CenterContinuous),
"distributed" => Ok(HorizontalAlignment::Distributed),
"fill" => Ok(HorizontalAlignment::Fill),
"general" => Ok(HorizontalAlignment::General),
"justify" => Ok(HorizontalAlignment::Justify),
"left" => Ok(HorizontalAlignment::Left),
"right" => Ok(HorizontalAlignment::Right),
_ => Err(format!(
"Invalid value for horizontal alignment: '{value}'."
)),
}
}
fn vertical(value: &str) -> Result<VerticalAlignment, String> {
match value {
"bottom" => Ok(VerticalAlignment::Bottom),
"center" => Ok(VerticalAlignment::Center),
"distributed" => Ok(VerticalAlignment::Distributed),
"justify" => Ok(VerticalAlignment::Justify),
"top" => Ok(VerticalAlignment::Top),
_ => Err(format!("Invalid value for vertical alignment: '{value}'.")),
}
}
/// # A wrapper around [`Model`] for a spreadsheet end user.
/// UserModel is a wrapper around Model with undo/redo history, _diffs_ and automatic evaluation.
///
/// A diff in this context (or more correctly a _user diff_) is a change created by a user.
///
/// Automatic evaluation means that actions like setting a value on a cell or deleting a column
/// will evaluate the model if needed.
///
/// It is meant to be used by UI applications like Web IronCalc or TironCalc.
///
///
/// # Examples
///
/// ```rust
/// # use ironcalc_base::UserModel;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut model = UserModel::new_empty("model", "en", "UTC")?;
/// model.set_user_input(0, 1, 1, "=1+1")?;
/// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "2");
/// model.undo()?;
/// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "");
/// model.redo()?;
/// assert_eq!(model.get_formatted_cell_value(0, 1, 1)?, "2");
/// # Ok(())
/// # }
/// ```
pub struct UserModel {
/// The underlying model
/// See also:
/// * [Model]
pub model: Model,
history: History,
send_queue: Vec<QueueDiffs>,
pause_evaluation: bool,
}
impl Debug for UserModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UserModel").finish()
}
}
impl UserModel {
/// Creates a user model from an existing model
pub fn from_model(model: Model) -> UserModel {
UserModel {
model,
history: History::default(),
send_queue: vec![],
pause_evaluation: false,
}
}
/// Creates a new UserModel.
///
/// See also:
/// * [Model::new_empty]
pub fn new_empty(name: &str, locale_id: &str, timezone: &str) -> Result<UserModel, String> {
let model = Model::new_empty(name, locale_id, timezone)?;
Ok(UserModel {
model,
history: History::default(),
send_queue: vec![],
pause_evaluation: false,
})
}
/// Creates a model from it's internal representation
///
/// See also:
/// * [Model::from_bytes]
pub fn from_bytes(s: &[u8]) -> Result<UserModel, String> {
let model = Model::from_bytes(s)?;
Ok(UserModel {
model,
history: History::default(),
send_queue: vec![],
pause_evaluation: false,
})
}
/// Returns the internal representation of a model
///
/// See also:
/// * [Model::to_json_str]
pub fn to_bytes(&self) -> Vec<u8> {
self.model.to_bytes()
}
/// Undoes last change if any, places the change in the redo list and evaluates the model if needed
///
/// See also:
/// * [UserModel::redo]
pub fn undo(&mut self) -> Result<(), String> {
if let Some(diff_list) = self.history.undo() {
self.apply_undo_diff_list(&diff_list)?;
self.send_queue.push(QueueDiffs {
r#type: DiffType::Undo,
list: diff_list.clone(),
});
};
Ok(())
}
/// Redoes the last undone change, places the change in the undo list and evaluates the model if needed
///
/// See also:
/// * [UserModel::redo]
pub fn redo(&mut self) -> Result<(), String> {
if let Some(diff_list) = self.history.redo() {
self.apply_diff_list(&diff_list)?;
self.send_queue.push(QueueDiffs {
r#type: DiffType::Redo,
list: diff_list.clone(),
});
};
Ok(())
}
/// Returns true if there are items to be undone
pub fn can_undo(&self) -> bool {
!self.history.undo_stack.is_empty()
}
/// Returns true if there are items to be redone
pub fn can_redo(&self) -> bool {
!self.history.redo_stack.is_empty()
}
/// Pauses automatic evaluation.
///
/// See also:
/// * [UserModel::evaluate]
/// * [UserModel::resume_evaluation]
pub fn pause_evaluation(&mut self) {
self.pause_evaluation = true;
}
/// Resumes automatic evaluation.
///
/// See also:
/// * [UserModel::evaluate]
/// * [UserModel::pause_evaluation]
pub fn resume_evaluation(&mut self) {
self.pause_evaluation = false;
}
/// Forces an evaluation of the model
///
/// See also:
/// * [Model::evaluate]
/// * [UserModel::pause_evaluation]
pub fn evaluate(&mut self) {
self.model.evaluate()
}
/// Returns the list of pending diffs and removes them from the queue
///
/// This is used together with [apply_external_diffs](UserModel::apply_external_diffs) to keep two remote models
/// in sync.
///
/// See also:
/// * [UserModel::apply_external_diffs]
pub fn flush_send_queue(&mut self) -> Vec<u8> {
// This can never fail :O:
let q = bitcode::encode(&self.send_queue);
self.send_queue = vec![];
q
}
/// This are external diffs that need to be applied to the model
///
/// This is used together with [flush_send_queue](UserModel::flush_send_queue) to keep two remote models in sync
///
/// See also:
/// * [UserModel::flush_send_queue]
pub fn apply_external_diffs(&mut self, diff_list_str: &[u8]) -> Result<(), String> {
if let Ok(queue_diffs_list) = bitcode::decode::<Vec<QueueDiffs>>(diff_list_str) {
for queue_diff in queue_diffs_list {
if matches!(queue_diff.r#type, DiffType::Redo) {
self.apply_diff_list(&queue_diff.list)?;
} else {
self.apply_undo_diff_list(&queue_diff.list)?;
}
}
} else {
return Err("Error parsing diff list".to_string());
}
Ok(())
}
/// Set the input in a cell
///
/// See also:
/// * [Model::set_user_input]
pub fn set_user_input(
&mut self,
sheet: u32,
row: i32,
column: i32,
value: &str,
) -> Result<(), String> {
if !is_valid_column_number(column) {
return Err("Invalid column".to_string());
}
if !is_valid_row(row) {
return Err("Invalid row".to_string());
}
let old_value = self
.model
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned();
self.model
.set_user_input(sheet, row, column, value.to_string());
self.evaluate_if_not_paused();
let diff_list = vec![Diff::SetCellValue {
sheet,
row,
column,
new_value: value.to_string(),
old_value: Box::new(old_value),
}];
self.push_diff_list(diff_list);
Ok(())
}
/// Returns the content of a cell
///
/// See also:
/// * [Model::get_cell_content]
#[inline]
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String, String> {
self.model.get_cell_content(sheet, row, column)
}
/// Returns the formatted value of a cell
///
/// See also:
/// * [Model::get_formatted_cell_value]
#[inline]
pub fn get_formatted_cell_value(
&self,
sheet: u32,
row: i32,
column: i32,
) -> Result<String, String> {
self.model.get_formatted_cell_value(sheet, row, column)
}
/// Returns the type of the cell
///
/// See also
/// * [Model::get_cell_type]
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<CellType, String> {
self.model.get_cell_type(sheet, row, column)
}
/// Adds new sheet
///
/// See also:
/// * [Model::new_sheet]
pub fn new_sheet(&mut self) {
let (name, index) = self.model.new_sheet();
self.push_diff_list(vec![Diff::NewSheet { index, name }]);
}
/// Deletes sheet by index
///
/// See also:
/// * [Model::delete_sheet]
pub fn delete_sheet(&mut self, sheet: u32) -> Result<(), String> {
self.push_diff_list(vec![Diff::DeleteSheet { sheet }]);
// There is no coming back
self.history.clear();
self.model.delete_sheet(sheet)?;
Ok(())
}
/// Renames a sheet by index
///
/// See also:
/// * [Model::rename_sheet_by_index]
pub fn rename_sheet(&mut self, sheet: u32, new_name: &str) -> Result<(), String> {
let old_value = self.model.workbook.worksheet(sheet)?.name.clone();
self.model.rename_sheet_by_index(sheet, new_name)?;
self.push_diff_list(vec![Diff::RenameSheet {
index: sheet,
old_value,
new_value: new_name.to_string(),
}]);
Ok(())
}
/// Sets sheet color
///
/// Note: an empty string will remove the color
///
/// See also
/// * [Model::set_sheet_color]
/// * [UserModel::get_worksheets_properties]
pub fn set_sheet_color(&mut self, sheet: u32, color: &str) -> Result<(), String> {
let old_value = match &self.model.workbook.worksheet(sheet)?.color {
Some(c) => c.clone(),
None => "".to_string(),
};
self.model.set_sheet_color(sheet, color)?;
self.push_diff_list(vec![Diff::SetSheetColor {
index: sheet,
old_value,
new_value: color.to_string(),
}]);
Ok(())
}
/// Removes cells contents and style
///
/// See also:
/// * [Model::cell_clear_all]
pub fn range_clear_all(&mut self, range: &Area) -> Result<(), String> {
let sheet = range.sheet;
let mut diff_list = Vec::new();
for row in range.row..range.row + range.height {
for column in range.column..range.column + range.width {
let old_value = self
.model
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned();
let old_style = self.model.get_style_for_cell(sheet, row, column);
self.model.cell_clear_all(sheet, row, column)?;
diff_list.push(Diff::CellClearAll {
sheet,
row,
column,
old_value: Box::new(old_value),
old_style: Box::new(old_style),
});
}
}
self.push_diff_list(diff_list);
Ok(())
}
/// Deletes the content in cells, but keeps the style
///
/// See also:
/// * [Model::cell_clear_contents]
pub fn range_clear_contents(&mut self, range: &Area) -> Result<(), String> {
let sheet = range.sheet;
let mut diff_list = Vec::new();
for row in range.row..range.row + range.height {
for column in range.column..range.column + range.width {
let old_value = self
.model
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned();
self.model.cell_clear_contents(sheet, row, column)?;
diff_list.push(Diff::CellClearContents {
sheet,
row,
column,
old_value: Box::new(old_value),
});
}
}
self.push_diff_list(diff_list);
Ok(())
}
/// Inserts a row
///
/// See also:
/// * [Model::insert_rows]
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), String> {
let diff_list = vec![Diff::InsertRow { sheet, row }];
self.push_diff_list(diff_list);
self.model.insert_rows(sheet, row, 1)?;
self.model.evaluate();
Ok(())
}
/// Deletes a row
///
/// See also:
/// * [Model::delete_rows]
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), String> {
let mut row_data = None;
let worksheet = self.model.workbook.worksheet(sheet)?;
for rd in &worksheet.rows {
if rd.r == row {
row_data = Some(rd.clone());
break;
}
}
let data = worksheet.sheet_data.get(&row).unwrap().clone();
let old_data = Box::new(RowData {
row: row_data,
data,
});
let diff_list = vec![Diff::DeleteRow {
sheet,
row,
old_data,
}];
self.push_diff_list(diff_list);
self.model.delete_rows(sheet, row, 1)?;
self.model.evaluate();
Ok(())
}
/// Inserts a column
///
/// See also:
/// * [Model::insert_columns]
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), String> {
let diff_list = vec![Diff::InsertColumn { sheet, column }];
self.push_diff_list(diff_list);
self.model.insert_columns(sheet, column, 1)?;
self.model.evaluate();
Ok(())
}
/// Deletes a column
///
/// See also:
/// * [Model::delete_columns]
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), String> {
let worksheet = self.model.workbook.worksheet(sheet)?;
if !is_valid_column_number(column) {
return Err(format!("Column number '{column}' is not valid."));
}
let mut column_data = None;
for col in &worksheet.cols {
let min = col.min;
let max = col.max;
if column >= min && column <= max {
column_data = Some(Col {
min: column,
max: column,
width: col.width,
custom_width: col.custom_width,
style: col.style,
});
break;
}
}
let mut data = HashMap::new();
for (row, row_data) in &worksheet.sheet_data {
if let Some(cell) = row_data.get(&column) {
data.insert(*row, cell.clone());
}
}
let diff_list = vec![Diff::DeleteColumn {
sheet,
column,
old_data: Box::new(ColumnData {
column: column_data,
data,
}),
}];
self.push_diff_list(diff_list);
self.model.delete_columns(sheet, column, 1)?;
self.model.evaluate();
Ok(())
}
/// Sets the width of a column
///
/// See also:
/// * [Model::set_column_width]
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), String> {
let old_value = self.model.get_column_width(sheet, column)?;
self.push_diff_list(vec![Diff::SetColumnWidth {
sheet,
column,
new_value: width,
old_value,
}]);
self.model.set_column_width(sheet, column, width)
}
/// Sets the height of a row
///
/// See also:
/// * [Model::set_row_height]
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<(), String> {
let old_value = self.model.get_row_height(sheet, row)?;
self.push_diff_list(vec![Diff::SetRowHeight {
sheet,
row,
new_value: height,
old_value,
}]);
self.model.set_row_height(sheet, row, height)
}
/// Gets the height of a row
///
/// See also:
/// * [Model::get_row_height]
#[inline]
pub fn get_row_height(&self, sheet: u32, row: i32) -> Result<f64, String> {
self.model.get_row_height(sheet, row)
}
/// Gets the width of a column
///
/// See also:
/// * [Model::get_column_width]
#[inline]
pub fn get_column_width(&self, sheet: u32, column: i32) -> Result<f64, String> {
self.model.get_column_width(sheet, column)
}
/// Returns the number of frozen rows in the sheet
///
/// See also:
/// * [Model::get_frozen_rows_count()]
#[inline]
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32, String> {
self.model.get_frozen_rows_count(sheet)
}
/// Returns the number of frozen columns in the sheet
///
/// See also:
/// * [Model::get_frozen_columns_count()]
#[inline]
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32, String> {
self.model.get_frozen_columns_count(sheet)
}
/// Sets the number of frozen rows in sheet
///
/// See also:
/// * [Model::set_frozen_rows()]
pub fn set_frozen_rows_count(&mut self, sheet: u32, frozen_rows: i32) -> Result<(), String> {
let old_value = self.model.get_frozen_rows_count(sheet)?;
self.push_diff_list(vec![Diff::SetFrozenRowsCount {
sheet,
new_value: frozen_rows,
old_value,
}]);
self.model.set_frozen_rows(sheet, frozen_rows)
}
/// Sets the number of frozen columns in sheet
///
/// See also:
/// * [Model::set_frozen_columns()]
pub fn set_frozen_columns_count(
&mut self,
sheet: u32,
frozen_columns: i32,
) -> Result<(), String> {
let old_value = self.model.get_frozen_columns_count(sheet)?;
self.push_diff_list(vec![Diff::SetFrozenColumnsCount {
sheet,
new_value: frozen_columns,
old_value,
}]);
self.model.set_frozen_columns(sheet, frozen_columns)
}
/// Updates the range with a cell style.
/// See also:
/// * [Model::set_cell_style]
pub fn update_range_style(
&mut self,
range: &Area,
style_path: &str,
value: &str,
) -> Result<(), String> {
let sheet = range.sheet;
let mut diff_list = Vec::new();
for row in range.row..range.row + range.height {
for column in range.column..range.column + range.width {
let old_value = self.model.get_style_for_cell(sheet, row, column);
let mut style = old_value.clone();
match style_path {
"font.b" => {
style.font.b = boolean(value)?;
}
"font.i" => {
style.font.i = boolean(value)?;
}
"font.u" => {
style.font.u = boolean(value)?;
}
"font.strike" => {
style.font.strike = boolean(value)?;
}
"font.color" => {
style.font.color = color(value)?;
}
"fill.bg_color" => {
style.fill.bg_color = color(value)?;
}
"fill.fg_color" => {
style.fill.fg_color = color(value)?;
}
"num_fmt" => {
style.num_fmt = value.to_owned();
}
"border.left" => {
style.border.left = border(value)?;
}
"border.right" => {
style.border.right = border(value)?;
}
"border.top" => {
style.border.top = border(value)?;
}
"border.bottom" => {
style.border.bottom = border(value)?;
}
"alignment" => {
if !value.is_empty() {
return Err(format!("Alignment must be empty, but found: '{value}'."));
}
style.alignment = None;
}
"alignment.horizontal" => match style.alignment {
Some(ref mut s) => s.horizontal = horizontal(value)?,
None => {
let alignment = Alignment {
horizontal: horizontal(value)?,
..Default::default()
};
style.alignment = Some(alignment)
}
},
"alignment.vertical" => match style.alignment {
Some(ref mut s) => s.vertical = vertical(value)?,
None => {
let alignment = Alignment {
vertical: vertical(value)?,
..Default::default()
};
style.alignment = Some(alignment)
}
},
"alignment.wrap_text" => match style.alignment {
Some(ref mut s) => s.wrap_text = boolean(value)?,
None => {
let alignment = Alignment {
wrap_text: boolean(value)?,
..Default::default()
};
style.alignment = Some(alignment)
}
},
_ => {
return Err(format!("Invalid style path: '{style_path}'."));
}
}
self.model.set_cell_style(sheet, row, column, &style)?;
diff_list.push(Diff::SetCellStyle {
sheet,
row,
column,
old_value: Box::new(old_value),
new_value: Box::new(style),
})
}
}
self.push_diff_list(diff_list);
Ok(())
}
/// Returns the style for a cell
///
/// See also:
/// * [Model::get_style_for_cell]
#[inline]
pub fn get_cell_style(&mut self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
Ok(self.model.get_style_for_cell(sheet, row, column))
}
/// Returns information about the sheets
///
/// See also:
/// * [Model::get_worksheets_properties]
#[inline]
pub fn get_worksheets_properties(&self) -> Vec<SheetProperties> {
self.model.get_worksheets_properties()
}
// **** Private methods ****** //
fn push_diff_list(&mut self, diff_list: DiffList) {
self.send_queue.push(QueueDiffs {
r#type: DiffType::Redo,
list: diff_list.clone(),
});
self.history.push(diff_list);
}
fn evaluate_if_not_paused(&mut self) {
if !self.pause_evaluation {
self.model.evaluate();
}
}
fn apply_undo_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> {
let mut needs_evaluation = false;
for diff in diff_list {
match diff {
Diff::SetCellValue {
sheet,
row,
column,
new_value: _,
old_value,
} => {
needs_evaluation = true;
match *old_value.clone() {
Some(value) => {
self.model
.workbook
.worksheet_mut(*sheet)?
.update_cell(*row, *column, value);
}
None => {
self.model.cell_clear_all(*sheet, *row, *column)?;
}
}
}
Diff::SetColumnWidth {
sheet,
column,
new_value: _,
old_value,
} => self.model.set_column_width(*sheet, *column, *old_value)?,
Diff::SetRowHeight {
sheet,
row,
new_value: _,
old_value,
} => self.model.set_row_height(*sheet, *row, *old_value)?,
Diff::CellClearContents {
sheet,
row,
column,
old_value,
} => {
needs_evaluation = true;
if let Some(value) = *old_value.clone() {
self.model
.workbook
.worksheet_mut(*sheet)?
.update_cell(*row, *column, value);
}
}
Diff::CellClearAll {
sheet,
row,
column,
old_value,
old_style,
} => {
needs_evaluation = true;
if let Some(value) = *old_value.clone() {
self.model
.workbook
.worksheet_mut(*sheet)?
.update_cell(*row, *column, value);
self.model
.set_cell_style(*sheet, *row, *column, old_style)?;
}
}
Diff::SetCellStyle {
sheet,
row,
column,
old_value,
new_value: _,
} => self
.model
.set_cell_style(*sheet, *row, *column, old_value)?,
Diff::InsertRow { sheet, row } => {
self.model.delete_rows(*sheet, *row, 1)?;
needs_evaluation = true;
}
Diff::DeleteRow {
sheet,
row,
old_data,
} => {
needs_evaluation = true;
self.model.insert_rows(*sheet, *row, 1)?;
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
if let Some(row_data) = old_data.row.clone() {
worksheet.rows.push(row_data);
}
worksheet.sheet_data.insert(*row, old_data.data.clone());
}
Diff::InsertColumn { sheet, column } => {
self.model.delete_columns(*sheet, *column, 1)?;
needs_evaluation = true;
}
Diff::DeleteColumn {
sheet,
column,
old_data,
} => {
needs_evaluation = true;
// inserts an empty column
self.model.insert_columns(*sheet, *column, 1)?;
// puts all the data back
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
for (row, cell) in &old_data.data {
worksheet.update_cell(*row, *column, cell.clone());
}
// makes sure that the width and style is correct
if let Some(col) = &old_data.column {
let width = col.width * constants::COLUMN_WIDTH_FACTOR;
let style = col.style;
worksheet.set_column_width_and_style(*column, width, style)?;
}
}
Diff::SetFrozenRowsCount {
sheet,
new_value: _,
old_value,
} => self.model.set_frozen_rows(*sheet, *old_value)?,
Diff::SetFrozenColumnsCount {
sheet,
new_value: _,
old_value,
} => self.model.set_frozen_columns(*sheet, *old_value)?,
Diff::DeleteSheet { sheet: _ } => {
// do nothing
}
Diff::NewSheet { index, name: _ } => {
self.model.delete_sheet(*index)?;
}
Diff::RenameSheet {
index,
old_value,
new_value: _,
} => {
self.model.rename_sheet_by_index(*index, old_value)?;
}
Diff::SetSheetColor {
index,
old_value,
new_value: _,
} => {
self.model.set_sheet_color(*index, old_value)?;
}
}
}
if needs_evaluation {
self.evaluate_if_not_paused();
}
Ok(())
}
/// Applies diff list
fn apply_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> {
let mut needs_evaluation = false;
for diff in diff_list {
match diff {
Diff::SetCellValue {
sheet,
row,
column,
new_value,
old_value: _,
} => {
needs_evaluation = true;
self.model
.set_user_input(*sheet, *row, *column, new_value.to_string());
}
Diff::SetColumnWidth {
sheet,
column,
new_value,
old_value: _,
} => {
self.model.set_column_width(*sheet, *column, *new_value)?;
}
Diff::SetRowHeight {
sheet,
row,
new_value,
old_value: _,
} => {
self.model.set_row_height(*sheet, *row, *new_value)?;
}
Diff::CellClearContents {
sheet,
row,
column,
old_value: _,
} => {
self.model.cell_clear_contents(*sheet, *row, *column)?;
needs_evaluation = true;
}
Diff::CellClearAll {
sheet,
row,
column,
old_value: _,
old_style: _,
} => {
self.model.cell_clear_all(*sheet, *row, *column)?;
needs_evaluation = true;
}
Diff::SetCellStyle {
sheet,
row,
column,
old_value: _,
new_value,
} => self
.model
.set_cell_style(*sheet, *row, *column, new_value)?,
Diff::InsertRow { sheet, row } => {
self.model.insert_rows(*sheet, *row, 1)?;
needs_evaluation = true;
}
Diff::DeleteRow {
sheet,
row,
old_data: _,
} => {
self.model.delete_rows(*sheet, *row, 1)?;
needs_evaluation = true;
}
Diff::InsertColumn { sheet, column } => {
needs_evaluation = true;
self.model.insert_columns(*sheet, *column, 1)?;
}
Diff::DeleteColumn {
sheet,
column,
old_data: _,
} => {
self.model.delete_columns(*sheet, *column, 1)?;
needs_evaluation = true;
}
Diff::SetFrozenRowsCount {
sheet,
new_value,
old_value: _,
} => self.model.set_frozen_rows(*sheet, *new_value)?,
Diff::SetFrozenColumnsCount {
sheet,
new_value,
old_value: _,
} => self.model.set_frozen_columns(*sheet, *new_value)?,
Diff::DeleteSheet { sheet } => self.model.delete_sheet(*sheet)?,
Diff::NewSheet { index, name } => {
self.model.insert_sheet(name, *index, None)?;
}
Diff::RenameSheet {
index,
old_value: _,
new_value,
} => {
self.model.rename_sheet_by_index(*index, new_value)?;
}
Diff::SetSheetColor {
index,
old_value: _,
new_value,
} => {
self.model.set_sheet_color(*index, new_value)?;
}
}
}
if needs_evaluation {
self.evaluate_if_not_paused();
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{
types::{HorizontalAlignment, VerticalAlignment},
user_model::{horizontal, vertical},
};
#[test]
fn test_vertical() {
let all = vec![
VerticalAlignment::Bottom,
VerticalAlignment::Center,
VerticalAlignment::Distributed,
VerticalAlignment::Justify,
VerticalAlignment::Top,
];
for a in all {
assert_eq!(vertical(&format!("{}", a)), Ok(a));
}
}
#[test]
fn test_horizontal() {
let all = vec![
HorizontalAlignment::Center,
HorizontalAlignment::CenterContinuous,
HorizontalAlignment::Distributed,
HorizontalAlignment::Fill,
HorizontalAlignment::General,
HorizontalAlignment::Justify,
HorizontalAlignment::Left,
HorizontalAlignment::Right,
];
for a in all {
assert_eq!(horizontal(&format!("{}", a)), Ok(a));
}
}
}