From 864a37f1e64b8cd32bcaa26f277aa69cd9333fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher=20Andr=C3=A9s?= Date: Sun, 2 Jun 2024 18:43:43 +0200 Subject: [PATCH] UPDATE: Adds autofill_columns (#74) --- base/src/test/user_model/mod.rs | 3 +- .../test/user_model/test_autofill_columns.rs | 404 ++++++++++++++++++ ...test_autofill.rs => test_autofill_rows.rs} | 0 base/src/user_model.rs | 101 +++++ bindings/wasm/fix_types.py | 54 +++ bindings/wasm/src/lib.rs | 13 + 6 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 base/src/test/user_model/test_autofill_columns.rs rename base/src/test/user_model/{test_autofill.rs => test_autofill_rows.rs} (100%) diff --git a/base/src/test/user_model/mod.rs b/base/src/test/user_model/mod.rs index d995372..1259726 100644 --- a/base/src/test/user_model/mod.rs +++ b/base/src/test/user_model/mod.rs @@ -1,5 +1,6 @@ mod test_add_delete_sheets; -mod test_autofill; +mod test_autofill_columns; +mod test_autofill_rows; mod test_clear_cells; mod test_diff_queue; mod test_evaluation; diff --git a/base/src/test/user_model/test_autofill_columns.rs b/base/src/test/user_model/test_autofill_columns.rs new file mode 100644 index 0000000..871dadc --- /dev/null +++ b/base/src/test/user_model/test_autofill_columns.rs @@ -0,0 +1,404 @@ +#![allow(clippy::unwrap_used)] + +use crate::constants::{LAST_COLUMN, LAST_ROW}; +use crate::expressions::types::Area; +use crate::test::util::new_empty_model; +use crate::UserModel; + +#[test] +fn basic_tests() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // This is cell A3 + model.set_user_input(0, 3, 1, "alpha").unwrap(); + // We autofill from A3 to C3 + model + .auto_fill_columns( + &Area { + sheet: 0, + row: 3, + column: 1, + width: 1, + height: 1, + }, + 5, + ) + .unwrap(); + // B3 + assert_eq!( + model.get_formatted_cell_value(0, 3, 2), + Ok("alpha".to_string()) + ); + // C3 + assert_eq!( + model.get_formatted_cell_value(0, 3, 3), + Ok("alpha".to_string()) + ); +} + +#[test] +fn one_cell_right() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + model.set_user_input(0, 1, 1, "23").unwrap(); + model + .auto_fill_columns( + &Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }, + 2, + ) + .unwrap(); + // B1 + assert_eq!( + model.get_formatted_cell_value(0, 1, 2), + Ok("23".to_string()) + ); +} + +#[test] +fn alpha_beta_gamma() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // cells A1:B3 + model.set_user_input(0, 1, 1, "Alpher").unwrap(); // A1 + model.set_user_input(0, 1, 2, "Bethe").unwrap(); // B1 + model.set_user_input(0, 1, 3, "Gamow").unwrap(); // C1 + model.set_user_input(0, 2, 1, "=A1").unwrap(); // A2 + model.set_user_input(0, 2, 2, "=B1").unwrap(); // B2 + model.set_user_input(0, 2, 3, "=C1").unwrap(); // C2 + + // We autofill from A1:C2 to I2 + model + .auto_fill_columns( + &Area { + sheet: 0, + row: 1, + column: 1, + width: 3, + height: 2, + }, + 9, + ) + .unwrap(); + + // D1 + assert_eq!( + model.get_formatted_cell_value(0, 1, 4), + Ok("Alpher".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 1, 5), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 1, 6), + Ok("Gamow".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 1, 7), + Ok("Alpher".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 1, 8), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 1, 9), + Ok("Gamow".to_string()) + ); + + assert_eq!( + model.get_formatted_cell_value(0, 2, 4), + Ok("Alpher".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 2, 5), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 2, 6), + Ok("Gamow".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 2, 7), + Ok("Alpher".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 2, 8), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 2, 9), + Ok("Gamow".to_string()) + ); + + assert_eq!(model.get_cell_content(0, 2, 4), Ok("=D1".to_string())); +} + +#[test] +fn styles() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // cells A1:C1 + model.set_user_input(0, 1, 1, "Alpher").unwrap(); + model.set_user_input(0, 2, 1, "Bethe").unwrap(); + model.set_user_input(0, 3, 1, "Gamow").unwrap(); + + let b1 = Area { + sheet: 0, + row: 1, + column: 2, + width: 1, + height: 1, + }; + + let c1 = Area { + sheet: 0, + row: 1, + column: 3, + width: 1, + height: 1, + }; + + model.update_range_style(&b1, "font.i", "true").unwrap(); + model + .update_range_style(&c1, "fill.bg_color", "#334455") + .unwrap(); + + model + .auto_fill_columns( + &Area { + sheet: 0, + row: 1, + column: 1, + width: 3, + height: 1, + }, + 9, + ) + .unwrap(); + + // Check that cell E1 has B1 style + let style = model.get_cell_style(0, 1, 5).unwrap(); + assert!(style.font.i); + // A6 would have the style of A3 + let style = model.get_cell_style(0, 1, 6).unwrap(); + assert_eq!(style.fill.bg_color, Some("#334455".to_string())); + + model.undo().unwrap(); + + assert_eq!(model.get_cell_content(0, 1, 4), Ok("".to_string())); + // Check that cell A5 has A2 style + let style = model.get_cell_style(0, 1, 5).unwrap(); + assert!(!style.font.i); + // A6 would have the style of A3 + let style = model.get_cell_style(0, 1, 6).unwrap(); + assert_eq!(style.fill.bg_color, None); + + model.redo().unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 1, 4), + Ok("Alpher".to_string()) + ); + // Check that cell A5 has A2 style + let style = model.get_cell_style(0, 1, 5).unwrap(); + assert!(style.font.i); + // A6 would have the style of A3 + let style = model.get_cell_style(0, 1, 6).unwrap(); + assert_eq!(style.fill.bg_color, Some("#334455".to_string())); +} + +#[test] +fn left() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // cells A10:A12 + model.set_user_input(0, 1, 10, "Alpher").unwrap(); + model.set_user_input(0, 1, 11, "Bethe").unwrap(); + model.set_user_input(0, 1, 12, "Gamow").unwrap(); + + // We fill upwards to row 5 + model + .auto_fill_columns( + &Area { + sheet: 0, + row: 1, + column: 10, + width: 3, + height: 1, + }, + 5, + ) + .unwrap(); + + assert_eq!( + model.get_formatted_cell_value(0, 1, 9), + Ok("Gamow".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 1, 8), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 1, 7), + Ok("Alpher".to_string()) + ); +} + +#[test] +fn left_4() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // cells A10:A13 + model.set_user_input(0, 1, 10, "Margaret Burbidge").unwrap(); + model.set_user_input(0, 1, 11, "Geoffrey Burbidge").unwrap(); + model.set_user_input(0, 1, 12, "Willy Fowler").unwrap(); + model.set_user_input(0, 1, 13, "Fred Hoyle").unwrap(); + + // We fill left to row 5 + model + .auto_fill_columns( + &Area { + sheet: 0, + row: 1, + column: 10, + width: 4, + height: 1, + }, + 5, + ) + .unwrap(); + + assert_eq!( + model.get_formatted_cell_value(0, 1, 9), + Ok("Fred Hoyle".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 1, 8), + Ok("Willy Fowler".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 1, 5), + Ok("Fred Hoyle".to_string()) + ); +} + +#[test] +fn errors() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + + model.set_user_input(0, 1, 4, "Margaret Burbidge").unwrap(); + + // Invalid sheet + assert_eq!( + model.auto_fill_columns( + &Area { + sheet: 3, + row: 1, + column: 4, + width: 1, + height: 1, + }, + 10, + ), + Err("Invalid worksheet index: '3'".to_string()) + ); + + // invalid column + assert_eq!( + model.auto_fill_columns( + &Area { + sheet: 0, + row: 1, + column: -1, + width: 1, + height: 1, + }, + 10, + ), + Err("Invalid column: '-1'".to_string()) + ); + + // invalid column + assert_eq!( + model.auto_fill_columns( + &Area { + sheet: 0, + row: 1, + column: LAST_COLUMN - 1, + width: 10, + height: 1, + }, + 10, + ), + Err("Invalid column: '16392'".to_string()) + ); + + assert_eq!( + model.auto_fill_columns( + &Area { + sheet: 0, + row: LAST_ROW + 1, + column: 1, + width: 10, + height: 1, + }, + 10, + ), + Err("Invalid row: '1048577'".to_string()) + ); + + assert_eq!( + model.auto_fill_columns( + &Area { + sheet: 0, + row: LAST_ROW - 2, + column: 1, + width: 1, + height: 10, + }, + 10, + ), + Err("Invalid row: '1048583'".to_string()) + ); + + assert_eq!( + model.auto_fill_columns( + &Area { + sheet: 0, + row: 1, + column: 5, + width: 10, + height: 1, + }, + -10, + ), + Err("Invalid row: '-10'".to_string()) + ); +} + +#[test] +fn invalid_parameters() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + model.set_user_input(0, 1, 1, "23").unwrap(); + assert_eq!( + model.auto_fill_columns( + &Area { + sheet: 0, + row: 1, + column: 1, + width: 2, + height: 1, + }, + 2, + ), + Err("Invalid parameters for autofill".to_string()) + ); +} diff --git a/base/src/test/user_model/test_autofill.rs b/base/src/test/user_model/test_autofill_rows.rs similarity index 100% rename from base/src/test/user_model/test_autofill.rs rename to base/src/test/user_model/test_autofill_rows.rs diff --git a/base/src/user_model.rs b/base/src/user_model.rs index 7320a18..9351f53 100644 --- a/base/src/user_model.rs +++ b/base/src/user_model.rs @@ -1044,6 +1044,107 @@ impl UserModel { Ok(()) } + /// Fills the cells from `source_area` until `to_column`. + /// This simulates the user clicking on the cell outline handle and dragging it to the right (or to the left) + pub fn auto_fill_columns(&mut self, source_area: &Area, to_column: i32) -> Result<(), String> { + let mut diff_list = Vec::new(); + let sheet = source_area.sheet; + let row1 = source_area.row; + let column1 = source_area.column; + let width1 = source_area.width; + let height1 = source_area.height; + + // Check first all parameters are valid + if self.model.workbook.worksheet(sheet).is_err() { + return Err(format!("Invalid worksheet index: '{sheet}'")); + } + + if !is_valid_column_number(column1) { + return Err(format!("Invalid column: '{column1}'")); + } + if !is_valid_row(row1) { + return Err(format!("Invalid row: '{row1}'")); + } + if !is_valid_column_number(column1 + width1 - 1) { + return Err(format!("Invalid column: '{}'", column1 + width1 - 1)); + } + if !is_valid_row(row1 + height1 - 1) { + return Err(format!("Invalid row: '{}'", row1 + height1 - 1)); + } + + if !is_valid_row(to_column) { + return Err(format!("Invalid row: '{to_column}'")); + } + + // anchor_column is the first column that repeats in each case. + let anchor_column; + let sign; + // this is the range of columns we are going to fill + let column_range: Vec; + + if to_column >= column1 + width1 { + // we go right, we start from `1 + width` to `to_column`, + anchor_column = column1; + sign = 1; + column_range = (column1 + width1..to_column + 1).collect(); + } else if to_column < column1 { + // we go left, starting from `column1 - `` all the way to `to_column` + anchor_column = column1 + width1 - 1; + sign = -1; + column_range = (to_column..column1).rev().collect(); + } else { + return Err("Invalid parameters for autofill".to_string()); + } + + for row in row1..row1 + height1 { + let mut index = 0; + for column_ref in &column_range { + let column = *column_ref; + // Save value and style first + let old_value = self + .model + .workbook + .worksheet(sheet)? + .cell(row, column) + .cloned(); + let old_style = self.model.get_style_for_cell(sheet, row, column); + + // compute the new value and set it + let source_column = anchor_column + index; + let target_value = self + .model + .extend_to(sheet, row, source_column, row, column)?; + self.model + .set_user_input(sheet, row, column, target_value.to_string()); + + // Compute the new style and set it + let new_style = self.model.get_style_for_cell(sheet, row, source_column); + self.model.set_cell_style(sheet, row, column, &new_style)?; + + // Add the diffs + diff_list.push(Diff::SetCellStyle { + sheet, + row, + column, + old_value: Box::new(old_style), + new_value: Box::new(new_style), + }); + diff_list.push(Diff::SetCellValue { + sheet, + row, + column, + new_value: target_value.to_string(), + old_value: Box::new(old_value), + }); + + index = (index + sign) % width1; + } + } + self.push_diff_list(diff_list); + self.evaluate(); + Ok(()) + } + /// Returns information about the sheets /// /// See also: diff --git a/bindings/wasm/fix_types.py b/bindings/wasm/fix_types.py index 41048f6..24b2bf8 100644 --- a/bindings/wasm/fix_types.py +++ b/bindings/wasm/fix_types.py @@ -63,15 +63,69 @@ style_types = r""" getCellStyle(sheet: number, row: number, column: number): CellStyle; """.strip() +view = r""" +* @returns {any} +*/ + getSelectedView(): any; +""".strip() + +view_types = r""" +* @returns {CellStyle} +*/ + getSelectedView(): SelectedView; +""".strip() + +autofill_rows = r""" +/** +* @param {any} source_area +* @param {number} to_row +*/ + autoFillRows(source_area: any, to_row: number): void; +} +""" + +autofill_rows_types = r""" +/** +* @param {Area} source_area +* @param {number} to_row +*/ + autoFillRows(source_area: Area, to_row: number): void; +} +""" + +autofill_columns = r""" +/** +* @param {any} source_area +* @param {number} to_column +*/ + autoFillColumns(source_area: any, to_column: number): void; +} +""" + +autofill_columns_types = r""" +/** +* @param {Area} source_area +* @param {number} to_column +*/ + autoFillColumns(source_area: Area, to_column: number): void; +} +""" + def fix_types(text): text = text.replace(get_tokens_str, get_tokens_str_types) text = text.replace(update_style_str, update_style_str_types) text = text.replace(properties, properties_types) text = text.replace(style, style_types) + text = text.replace(view, view_types) + text = text.replace(autofill_rows, autofill_rows_types) + text = text.replace(autofill_columns, autofill_columns_types) with open("types.ts") as f: types_str = f.read() header_types = "{}\n\n{}".format(header, types_str) text = text.replace(header, header_types) + if text.find("any") != -1: + print("There are 'unfixed' types. Please check.") + exit(1) return text diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 72832dc..8a08e1f 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -363,4 +363,17 @@ impl Model { .auto_fill_rows(&area, to_row) .map_err(to_js_error) } + + #[wasm_bindgen(js_name = "autoFillColumns")] + pub fn auto_fill_columns( + &mut self, + source_area: JsValue, + to_column: i32, + ) -> Result<(), JsError> { + let area: Area = + serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?; + self.model + .auto_fill_columns(&area, to_column) + .map_err(to_js_error) + } }