diff --git a/base/src/test/user_model/mod.rs b/base/src/test/user_model/mod.rs index 646abfc..d995372 100644 --- a/base/src/test/user_model/mod.rs +++ b/base/src/test/user_model/mod.rs @@ -1,4 +1,5 @@ mod test_add_delete_sheets; +mod test_autofill; mod test_clear_cells; mod test_diff_queue; mod test_evaluation; diff --git a/base/src/test/user_model/test_autofill.rs b/base/src/test/user_model/test_autofill.rs new file mode 100644 index 0000000..d345dea --- /dev/null +++ b/base/src/test/user_model/test_autofill.rs @@ -0,0 +1,399 @@ +#![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 A5 + model + .auto_fill_rows( + &Area { + sheet: 0, + row: 3, + column: 1, + width: 1, + height: 1, + }, + 5, + ) + .unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 4, 1), + Ok("alpha".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 5, 1), + Ok("alpha".to_string()) + ); +} + +#[test] +fn one_cell_down() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + model.set_user_input(0, 1, 1, "23").unwrap(); + model + .auto_fill_rows( + &Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }, + 2, + ) + .unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 2, 1), + 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(); + model.set_user_input(0, 2, 1, "Bethe").unwrap(); + model.set_user_input(0, 3, 1, "Gamow").unwrap(); + model.set_user_input(0, 1, 2, "=A1").unwrap(); + model.set_user_input(0, 2, 2, "=A2").unwrap(); + model.set_user_input(0, 3, 2, "=A3").unwrap(); + // We autofill from A1:B3 to A9 + model + .auto_fill_rows( + &Area { + sheet: 0, + row: 1, + column: 1, + width: 2, + height: 3, + }, + 9, + ) + .unwrap(); + + assert_eq!( + model.get_formatted_cell_value(0, 4, 1), + Ok("Alpher".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 5, 1), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 6, 1), + Ok("Gamow".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 7, 1), + Ok("Alpher".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 8, 1), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 9, 1), + Ok("Gamow".to_string()) + ); + + assert_eq!( + model.get_formatted_cell_value(0, 4, 2), + Ok("Alpher".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 5, 2), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 6, 2), + Ok("Gamow".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 7, 2), + Ok("Alpher".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 8, 2), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 9, 2), + Ok("Gamow".to_string()) + ); + + assert_eq!(model.get_cell_content(0, 4, 2), Ok("=A4".to_string())); +} + +#[test] +fn styles() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // cells A1:B3 + 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 a2 = Area { + sheet: 0, + row: 2, + column: 1, + width: 1, + height: 1, + }; + + let a3 = Area { + sheet: 0, + row: 3, + column: 1, + width: 1, + height: 1, + }; + + model.update_range_style(&a2, "font.i", "true").unwrap(); + model + .update_range_style(&a3, "fill.bg_color", "#334455") + .unwrap(); + + model + .auto_fill_rows( + &Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 3, + }, + 9, + ) + .unwrap(); + + // Check that cell A5 has A2 style + let style = model.get_cell_style(0, 5, 1).unwrap(); + assert!(style.font.i); + // A6 would have the style of A3 + let style = model.get_cell_style(0, 6, 1).unwrap(); + assert_eq!(style.fill.bg_color, Some("#334455".to_string())); + + model.undo().unwrap(); + + assert_eq!(model.get_cell_content(0, 4, 1), Ok("".to_string())); + // Check that cell A5 has A2 style + let style = model.get_cell_style(0, 5, 1).unwrap(); + assert!(!style.font.i); + // A6 would have the style of A3 + let style = model.get_cell_style(0, 6, 1).unwrap(); + assert_eq!(style.fill.bg_color, None); + + model.redo().unwrap(); + assert_eq!( + model.get_formatted_cell_value(0, 4, 1), + Ok("Alpher".to_string()) + ); + // Check that cell A5 has A2 style + let style = model.get_cell_style(0, 5, 1).unwrap(); + assert!(style.font.i); + // A6 would have the style of A3 + let style = model.get_cell_style(0, 6, 1).unwrap(); + assert_eq!(style.fill.bg_color, Some("#334455".to_string())); +} + +#[test] +fn upwards() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // cells A10:A12 + model.set_user_input(0, 10, 1, "Alpher").unwrap(); + model.set_user_input(0, 11, 1, "Bethe").unwrap(); + model.set_user_input(0, 12, 1, "Gamow").unwrap(); + + // We fill upwards to row 5 + model + .auto_fill_rows( + &Area { + sheet: 0, + row: 10, + column: 1, + width: 1, + height: 3, + }, + 5, + ) + .unwrap(); + + assert_eq!( + model.get_formatted_cell_value(0, 9, 1), + Ok("Gamow".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 8, 1), + Ok("Bethe".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 7, 1), + Ok("Alpher".to_string()) + ); +} + +#[test] +fn upwards_4() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // cells A10:A13 + model.set_user_input(0, 10, 1, "Margaret Burbidge").unwrap(); + model.set_user_input(0, 11, 1, "Geoffrey Burbidge").unwrap(); + model.set_user_input(0, 12, 1, "Willy Fowler").unwrap(); + model.set_user_input(0, 13, 1, "Fred Hoyle").unwrap(); + + // We fill upwards to row 5 + model + .auto_fill_rows( + &Area { + sheet: 0, + row: 10, + column: 1, + width: 1, + height: 4, + }, + 5, + ) + .unwrap(); + + assert_eq!( + model.get_formatted_cell_value(0, 9, 1), + Ok("Fred Hoyle".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 8, 1), + Ok("Willy Fowler".to_string()) + ); + assert_eq!( + model.get_formatted_cell_value(0, 5, 1), + Ok("Fred Hoyle".to_string()) + ); +} + +#[test] +fn errors() { + let model = new_empty_model(); + let mut model = UserModel::from_model(model); + // cells A10:A13 + model.set_user_input(0, 4, 1, "Margaret Burbidge").unwrap(); + + // Invalid sheet + assert_eq!( + model.auto_fill_rows( + &Area { + sheet: 3, + row: 4, + column: 1, + width: 1, + height: 1, + }, + 10, + ), + Err("Invalid worksheet index: '3'".to_string()) + ); + + // invalid row + assert_eq!( + model.auto_fill_rows( + &Area { + sheet: 0, + row: -1, + column: 1, + width: 1, + height: 1, + }, + 10, + ), + Err("Invalid row: '-1'".to_string()) + ); + + // invalid row + assert_eq!( + model.auto_fill_rows( + &Area { + sheet: 0, + row: LAST_ROW - 1, + column: 1, + width: 1, + height: 10, + }, + 10, + ), + Err("Invalid row: '1048584'".to_string()) + ); + + assert_eq!( + model.auto_fill_rows( + &Area { + sheet: 0, + row: 1, + column: LAST_COLUMN + 1, + width: 1, + height: 10, + }, + 10, + ), + Err("Invalid column: '16385'".to_string()) + ); + + assert_eq!( + model.auto_fill_rows( + &Area { + sheet: 0, + row: 1, + column: LAST_COLUMN - 2, + width: 10, + height: 1, + }, + 10, + ), + Err("Invalid column: '16391'".to_string()) + ); + + assert_eq!( + model.auto_fill_rows( + &Area { + sheet: 0, + row: 5, + column: 1, + width: 1, + height: 10, + }, + -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_rows( + &Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 2, + }, + 2, + ), + Err("Invalid parameters for autofill".to_string()) + ); +} diff --git a/base/src/user_model.rs b/base/src/user_model.rs index 7fb7bbb..7320a18 100644 --- a/base/src/user_model.rs +++ b/base/src/user_model.rs @@ -927,7 +927,7 @@ impl UserModel { column, old_value: Box::new(old_value), new_value: Box::new(style), - }) + }); } } self.push_diff_list(diff_list); @@ -943,6 +943,107 @@ impl UserModel { Ok(self.model.get_style_for_cell(sheet, row, column)) } + /// Fills the cells from `source_area` until `to_row`. + /// This simulates the user clicking on the cell outline handle and dragging it downwards (or upwards) + pub fn auto_fill_rows(&mut self, source_area: &Area, to_row: 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_row) { + return Err(format!("Invalid row: '{to_row}'")); + } + + // anchor_row is the first row that repeats in each case. + let anchor_row; + let sign; + // this is the range of rows we are going to fill + let row_range: Vec; + + if to_row >= row1 + height1 { + // we go downwards, we start from `row1 + height1` to `to_row`, + anchor_row = row1; + sign = 1; + row_range = (row1 + height1..to_row + 1).collect(); + } else if to_row < row1 { + // we go upwards, starting from `row1 - `` all the way to `to_row` + anchor_row = row1 + height1 - 1; + sign = -1; + row_range = (to_row..row1).rev().collect(); + } else { + return Err("Invalid parameters for autofill".to_string()); + } + + for column in column1..column1 + width1 { + let mut index = 0; + for row_ref in &row_range { + // Save value and style first + let row = *row_ref; + 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_row = anchor_row + index; + let target_value = self + .model + .extend_to(sheet, source_row, 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, source_row, 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) % height1; + } + } + self.push_diff_list(diff_list); + self.evaluate(); + Ok(()) + } + /// Returns information about the sheets /// /// See also: @@ -1028,7 +1129,7 @@ impl UserModel { if !is_valid_column_number(column) { return Err(format!("Invalid column: '{column}'")); } - if !is_valid_column_number(row) { + if !is_valid_row(row) { return Err(format!("Invalid row: '{row}'")); } if self.model.workbook.worksheet(sheet).is_err() { diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 0af5018..72832dc 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -354,4 +354,13 @@ impl Model { pub fn get_show_grid_lines(&mut self, sheet: u32) -> Result { self.model.get_show_grid_lines(sheet).map_err(to_js_error) } + + #[wasm_bindgen(js_name = "autoFillRows")] + pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: 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_rows(&area, to_row) + .map_err(to_js_error) + } } diff --git a/bindings/wasm/tests/test.mjs b/bindings/wasm/tests/test.mjs index ab31c81..5b32dfc 100644 --- a/bindings/wasm/tests/test.mjs +++ b/bindings/wasm/tests/test.mjs @@ -119,5 +119,14 @@ test("floating column numbers get truncated", () => { assert.strictEqual(model.getRowHeight(0, 5), 100.5); }); +test("autofill", () => { + const model = new Model('en', 'UTC'); + model.setUserInput(0, 1, 1, "23"); + model.autoFillRows({sheet: 0, row: 1, column: 1, width: 1, height: 1}, 2); + + const result = model.getFormattedCellValue(0, 2, 1); + assert.strictEqual(result, "23"); +}); +