From c2777c73acc28ef022e2d90ae895f7cae413dbc6 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Mon, 30 Jun 2025 10:24:17 -0700 Subject: [PATCH] feat: implement move column move row with tests --- base/src/actions.rs | 151 ++++++++++++++++++- base/src/expressions/parser/stringify.rs | 20 +++ base/src/test/test_actions.rs | 178 ++++++++++++++++++++++- base/src/user_model/common.rs | 15 ++ bindings/wasm/src/lib.rs | 19 +++ bindings/wasm/tests/test.mjs | 38 +++++ 6 files changed, 418 insertions(+), 3 deletions(-) diff --git a/base/src/actions.rs b/base/src/actions.rs index fa4ddf9..aa82c17 100644 --- a/base/src/actions.rs +++ b/base/src/actions.rs @@ -457,7 +457,7 @@ impl Model { /// * Column is one of the extremes of the range. The new extreme would be target_column. /// Range is then normalized /// * Any other case, range is left unchanged. - /// NOTE: This does NOT move the data in the columns or move the colum styles + /// NOTE: This moves the data and column styles along with the formulas pub fn move_column_action( &mut self, sheet: u32, @@ -473,7 +473,70 @@ impl Model { return Err("Initial column out of boundaries".to_string()); } - // TODO: Add the actual displacement of data and styles + if delta == 0 { + return Ok(()); + } + + // Preserve cell contents, width and style of the column being moved + let original_refs = self + .workbook + .worksheet(sheet)? + .column_cell_references(column)?; + let mut original_cells = Vec::new(); + for r in &original_refs { + let cell = self + .workbook + .worksheet(sheet)? + .cell(r.row, column) + .ok_or("Expected Cell to exist")?; + let style_idx = cell.get_style(); + let formula_or_value = self + .get_cell_formula(sheet, r.row, column)? + .unwrap_or_else(|| cell.get_text(&self.workbook.shared_strings, &self.language)); + original_cells.push((r.row, formula_or_value, style_idx)); + self.cell_clear_all(sheet, r.row, column)?; + } + + let width = self.workbook.worksheet(sheet)?.get_column_width(column)?; + let style = self.workbook.worksheet(sheet)?.get_column_style(column)?; + + if delta > 0 { + for c in column + 1..=target_column { + let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?; + for r in refs { + self.move_cell(sheet, r.row, c, r.row, c - 1)?; + } + + let w = self.workbook.worksheet(sheet)?.get_column_width(c)?; + let s = self.workbook.worksheet(sheet)?.get_column_style(c)?; + self.workbook + .worksheet_mut(sheet)? + .set_column_width_and_style(c - 1, w, s)?; + } + } else { + for c in (target_column..=column - 1).rev() { + let refs = self.workbook.worksheet(sheet)?.column_cell_references(c)?; + for r in refs { + self.move_cell(sheet, r.row, c, r.row, c + 1)?; + } + + let w = self.workbook.worksheet(sheet)?.get_column_width(c)?; + let s = self.workbook.worksheet(sheet)?.get_column_style(c)?; + self.workbook + .worksheet_mut(sheet)? + .set_column_width_and_style(c + 1, w, s)?; + } + } + + for (r, value, style_idx) in original_cells { + self.set_user_input(sheet, r, target_column, value)?; + self.workbook + .worksheet_mut(sheet)? + .set_cell_style(r, target_column, style_idx)?; + } + self.workbook + .worksheet_mut(sheet)? + .set_column_width_and_style(target_column, width, style)?; // Update all formulas in the workbook self.displace_cells( @@ -486,4 +549,88 @@ impl Model { Ok(()) } + + /// Displaces cells due to a move row action + /// from initial_row to target_row = initial_row + row_delta + /// References will be updated following the same rules as move_column_action + /// NOTE: This moves the data and row styles along with the formulas + pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> { + // Check boundaries + let target_row = row + delta; + if !(1..=LAST_ROW).contains(&target_row) { + return Err("Target row out of boundaries".to_string()); + } + if !(1..=LAST_ROW).contains(&row) { + return Err("Initial row out of boundaries".to_string()); + } + + if delta == 0 { + return Ok(()); + } + + let original_cols = self.get_columns_for_row(sheet, row, false)?; + let mut original_cells = Vec::new(); + for c in &original_cols { + let cell = self + .workbook + .worksheet(sheet)? + .cell(row, *c) + .ok_or("Expected Cell to exist")?; + let style_idx = cell.get_style(); + let formula_or_value = self + .get_cell_formula(sheet, row, *c)? + .unwrap_or_else(|| cell.get_text(&self.workbook.shared_strings, &self.language)); + original_cells.push((*c, formula_or_value, style_idx)); + self.cell_clear_all(sheet, row, *c)?; + } + + if delta > 0 { + for r in row + 1..=target_row { + let cols = self.get_columns_for_row(sheet, r, false)?; + for c in cols { + self.move_cell(sheet, r, c, r - 1, c)?; + } + } + } else { + for r in (target_row..=row - 1).rev() { + let cols = self.get_columns_for_row(sheet, r, false)?; + for c in cols { + self.move_cell(sheet, r, c, r + 1, c)?; + } + } + } + + for (c, value, style_idx) in original_cells { + self.set_user_input(sheet, target_row, c, value)?; + self.workbook + .worksheet_mut(sheet)? + .set_cell_style(target_row, c, style_idx)?; + } + + let worksheet = &mut self.workbook.worksheets[sheet as usize]; + let mut new_rows = Vec::new(); + for r in worksheet.rows.iter() { + if r.r == row { + let mut nr = r.clone(); + nr.r = target_row; + new_rows.push(nr); + } else if delta > 0 && r.r > row && r.r <= target_row { + let mut nr = r.clone(); + nr.r -= 1; + new_rows.push(nr); + } else if delta < 0 && r.r < row && r.r >= target_row { + let mut nr = r.clone(); + nr.r += 1; + new_rows.push(nr); + } else { + new_rows.push(r.clone()); + } + } + worksheet.rows = new_rows; + + // Update all formulas in the workbook + self.displace_cells(&(DisplaceData::RowMove { sheet, row, delta }))?; + + Ok(()) + } } diff --git a/base/src/expressions/parser/stringify.rs b/base/src/expressions/parser/stringify.rs index f4bda5d..b1bbbc2 100644 --- a/base/src/expressions/parser/stringify.rs +++ b/base/src/expressions/parser/stringify.rs @@ -28,6 +28,11 @@ pub enum DisplaceData { column: i32, delta: i32, }, + RowMove { + sheet: u32, + row: i32, + delta: i32, + }, ColumnMove { sheet: u32, column: i32, @@ -159,6 +164,21 @@ pub(crate) fn stringify_reference( } } } + DisplaceData::RowMove { + sheet, + row: move_row, + delta, + } => { + if sheet_index == *sheet { + if row == *move_row { + row += *delta; + } else if (*delta > 0 && row > *move_row && row <= *move_row + *delta) + || (*delta < 0 && row < *move_row && row >= *move_row + *delta) + { + row -= *delta; + } + } + } DisplaceData::ColumnMove { sheet, column: move_column, diff --git a/base/src/test/test_actions.rs b/base/src/test/test_actions.rs index 5f89af6..463b57b 100644 --- a/base/src/test/test_actions.rs +++ b/base/src/test/test_actions.rs @@ -1,6 +1,6 @@ #![allow(clippy::unwrap_used)] -use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN}; +use crate::constants::{DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW}; use crate::model::Model; use crate::test::util::new_empty_model; use crate::types::Col; @@ -508,6 +508,10 @@ fn test_move_column_right() { assert_eq!(model._get_formula("E5"), "=SUM(H3:J7)"); assert_eq!(model._get_formula("E6"), "=SUM(H3:H7)"); assert_eq!(model._get_formula("E7"), "=SUM(G3:G7)"); + + // Data moved as well + assert_eq!(model._get_text("G1"), "1"); + assert_eq!(model._get_text("H1"), "3"); } #[test] @@ -532,5 +536,177 @@ fn tets_move_column_error() { assert!(result.is_ok()); } +#[test] +fn test_move_row_down() { + let mut model = new_empty_model(); + populate_table(&mut model); + // Formulas referencing rows 3 and 4 + model._set("E3", "=G3"); + model._set("E4", "=G4"); + model._set("E5", "=SUM(G3:J3)"); + model._set("E6", "=SUM(G3:G3)"); + model._set("E7", "=SUM(G4:G4)"); + model.evaluate(); + + // Move row 3 down by one position + let result = model.move_row_action(0, 3, 1); + assert!(result.is_ok()); + model.evaluate(); + + assert_eq!(model._get_formula("E3"), "=G3"); + assert_eq!(model._get_formula("E4"), "=G4"); + assert_eq!(model._get_formula("E5"), "=SUM(G4:J4)"); + assert_eq!(model._get_formula("E6"), "=SUM(G4:G4)"); + assert_eq!(model._get_formula("E7"), "=SUM(G3:G3)"); + + // Data moved as well + assert_eq!(model._get_text("G4"), "-2"); + assert_eq!(model._get_text("G3"), ""); +} + +#[test] +fn test_move_row_up() { + let mut model = new_empty_model(); + populate_table(&mut model); + // Formulas referencing rows 4 and 5 + model._set("E4", "=G4"); + model._set("E5", "=G5"); + model._set("E6", "=SUM(G4:J4)"); + model._set("E7", "=SUM(G4:G4)"); + model._set("E8", "=SUM(G5:G5)"); + model.evaluate(); + + // Move row 5 up by one position + let result = model.move_row_action(0, 5, -1); + assert!(result.is_ok()); + model.evaluate(); + + assert_eq!(model._get_formula("E4"), "=G4"); + assert_eq!(model._get_formula("E5"), "=G5"); + assert_eq!(model._get_formula("E6"), "=SUM(G5:J5)"); + assert_eq!(model._get_formula("E7"), "=SUM(G5:G5)"); + assert_eq!(model._get_formula("E8"), "=SUM(G4:G4)"); + + // Data moved as well + assert_eq!(model._get_text("G4"), ""); + assert_eq!(model._get_text("G5"), ""); +} + +#[test] +fn test_move_row_error() { + let mut model = new_empty_model(); + model.evaluate(); + + let result = model.move_row_action(0, 7, -10); + assert!(result.is_err()); + + let result = model.move_row_action(0, -7, 20); + assert!(result.is_err()); + + let result = model.move_row_action(0, LAST_ROW, 1); + assert!(result.is_err()); + + let result = model.move_row_action(0, LAST_ROW + 1, -10); + assert!(result.is_err()); + + // This works + let result = model.move_row_action(0, LAST_ROW, -1); + assert!(result.is_ok()); +} + +#[test] +fn test_move_row_down_absolute_refs() { + let mut model = new_empty_model(); + populate_table(&mut model); + // Absolute references + model._set("E3", "=$G$3"); + model._set("E4", "=$G$4"); + model._set("E5", "=SUM($G$3:$J$3)"); + model._set("E6", "=SUM($G$3:$G$3)"); + model._set("E7", "=SUM($G$4:$G$4)"); + model.evaluate(); + + assert!(model.move_row_action(0, 3, 1).is_ok()); + model.evaluate(); + + assert_eq!(model._get_formula("E3"), "=$G$3"); + assert_eq!(model._get_formula("E4"), "=$G$4"); + assert_eq!(model._get_formula("E5"), "=SUM($G$4:$J$4)"); + assert_eq!(model._get_formula("E6"), "=SUM($G$4:$G$4)"); + assert_eq!(model._get_formula("E7"), "=SUM($G$3:$G$3)"); +} + +#[test] +fn test_move_column_right_absolute_refs() { + let mut model = new_empty_model(); + populate_table(&mut model); + // Absolute references + model._set("E3", "=$G$3"); + model._set("E4", "=$H$3"); + model._set("E5", "=SUM($G$3:$J$7)"); + model._set("E6", "=SUM($G$3:$G$7)"); + model._set("E7", "=SUM($H$3:$H$7)"); + model.evaluate(); + + assert!(model.move_column_action(0, 7, 1).is_ok()); + model.evaluate(); + + assert_eq!(model._get_formula("E3"), "=$H$3"); + assert_eq!(model._get_formula("E4"), "=$G$3"); + assert_eq!(model._get_formula("E5"), "=SUM($H$3:$J$7)"); + assert_eq!(model._get_formula("E6"), "=SUM($H$3:$H$7)"); + assert_eq!(model._get_formula("E7"), "=SUM($G$3:$G$7)"); +} + +#[test] +fn test_move_row_down_mixed_refs() { + let mut model = new_empty_model(); + populate_table(&mut model); + model._set("E3", "=$G3"); // absolute col, relative row + model._set("E4", "=$G4"); + model._set("E5", "=SUM($G3:$J3)"); + model._set("E6", "=SUM($G3:$G3)"); + model._set("E7", "=SUM($G4:$G4)"); + model._set("F3", "=H$3"); // relative col, absolute row + model._set("F4", "=G$3"); + model.evaluate(); + + assert!(model.move_row_action(0, 3, 1).is_ok()); + model.evaluate(); + + assert_eq!(model._get_formula("E3"), "=$G3"); + assert_eq!(model._get_formula("E4"), "=$G4"); + assert_eq!(model._get_formula("E5"), "=SUM($G4:$J4)"); + assert_eq!(model._get_formula("E6"), "=SUM($G4:$G4)"); + assert_eq!(model._get_formula("E7"), "=SUM($G3:$G3)"); + assert_eq!(model._get_formula("F3"), "=G$4"); + assert_eq!(model._get_formula("F4"), "=H$4"); +} + +#[test] +fn test_move_column_right_mixed_refs() { + let mut model = new_empty_model(); + populate_table(&mut model); + model._set("E3", "=$G3"); + model._set("E4", "=$H3"); + model._set("E5", "=SUM($G3:$J7)"); + model._set("E6", "=SUM($G3:$G7)"); + model._set("E7", "=SUM($H3:$H7)"); + model._set("F3", "=H$3"); + model._set("F4", "=H$3"); + model.evaluate(); + + assert!(model.move_column_action(0, 7, 1).is_ok()); + model.evaluate(); + + assert_eq!(model._get_formula("E3"), "=$H3"); + assert_eq!(model._get_formula("E4"), "=$G3"); + assert_eq!(model._get_formula("E5"), "=SUM($H3:$J7)"); + assert_eq!(model._get_formula("E6"), "=SUM($H3:$H7)"); + assert_eq!(model._get_formula("E7"), "=SUM($G3:$G7)"); + assert_eq!(model._get_formula("F3"), "=G$3"); + assert_eq!(model._get_formula("F4"), "=G$3"); +} + // A B C D E F G H I J K L M N O P Q R // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 diff --git a/base/src/user_model/common.rs b/base/src/user_model/common.rs index a9cba54..d77753b 100644 --- a/base/src/user_model/common.rs +++ b/base/src/user_model/common.rs @@ -1013,6 +1013,21 @@ impl UserModel { Ok(()) } + /// Moves a column horizontally and adjusts formulas + pub fn move_column_action( + &mut self, + sheet: u32, + column: i32, + delta: i32, + ) -> Result<(), String> { + self.model.move_column_action(sheet, column, delta) + } + + /// Moves a row vertically and adjusts formulas + pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), String> { + self.model.move_row_action(sheet, row, delta) + } + /// Sets the width of a group of columns in a single diff list /// /// See also: diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index ab512e5..8517b0a 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -234,6 +234,25 @@ impl Model { .map_err(to_js_error) } + #[wasm_bindgen(js_name = "moveColumn")] + pub fn move_column_action( + &mut self, + sheet: u32, + column: i32, + delta: i32, + ) -> Result<(), JsError> { + self.model + .move_column_action(sheet, column, delta) + .map_err(to_js_error) + } + + #[wasm_bindgen(js_name = "moveRow")] + pub fn move_row_action(&mut self, sheet: u32, row: i32, delta: i32) -> Result<(), JsError> { + self.model + .move_row_action(sheet, row, delta) + .map_err(to_js_error) + } + #[wasm_bindgen(js_name = "setRowsHeight")] pub fn set_rows_height( &mut self, diff --git a/bindings/wasm/tests/test.mjs b/bindings/wasm/tests/test.mjs index 99dd0f6..79aa854 100644 --- a/bindings/wasm/tests/test.mjs +++ b/bindings/wasm/tests/test.mjs @@ -172,5 +172,43 @@ test('deleteColumns removes cells', () => { assert.strictEqual(model.getCellContent(0, 1, 2), ''); }); +test("move row", () => { + const model = new Model('Workbook1', 'en', 'UTC'); + model.setUserInput(0, 3, 5, "=G3"); + model.setUserInput(0, 4, 5, "=G4"); + model.setUserInput(0, 5, 5, "=SUM(G3:J3)"); + model.setUserInput(0, 6, 5, "=SUM(G3:G3)"); + model.setUserInput(0, 7, 5, "=SUM(G4:G4)"); + model.evaluate(); + + model.moveRow(0, 3, 1); + model.evaluate(); + + assert.strictEqual(model.getCellContent(0, 3, 5), "=G3"); + assert.strictEqual(model.getCellContent(0, 4, 5), "=G4"); + assert.strictEqual(model.getCellContent(0, 5, 5), "=SUM(G4:J4)"); + assert.strictEqual(model.getCellContent(0, 6, 5), "=SUM(G4:G4)"); + assert.strictEqual(model.getCellContent(0, 7, 5), "=SUM(G3:G3)"); +}); + +test("move column", () => { + const model = new Model('Workbook1', 'en', 'UTC'); + model.setUserInput(0, 3, 5, "=G3"); + model.setUserInput(0, 4, 5, "=H3"); + model.setUserInput(0, 5, 5, "=SUM(G3:J7)"); + model.setUserInput(0, 6, 5, "=SUM(G3:G7)"); + model.setUserInput(0, 7, 5, "=SUM(H3:H7)"); + model.evaluate(); + + model.moveColumn(0, 7, 1); + model.evaluate(); + + assert.strictEqual(model.getCellContent(0, 3, 5), "=H3"); + assert.strictEqual(model.getCellContent(0, 4, 5), "=G3"); + assert.strictEqual(model.getCellContent(0, 5, 5), "=SUM(H3:J7)"); + assert.strictEqual(model.getCellContent(0, 6, 5), "=SUM(H3:H7)"); + assert.strictEqual(model.getCellContent(0, 7, 5), "=SUM(G3:G7)"); +}); +