Merge pull request #112 from nihilist-narwhal/fix-shifted-cell-references
Insert/Delete Cells: adjust references like Excel
This commit is contained in:
commit
e92a67bbba
|
|
@ -74,20 +74,8 @@ fn shift_sheet_features(sh: &mut Sheet, axis: Axis, at: u32, count: u32, insert:
|
|||
None
|
||||
}
|
||||
};
|
||||
let map_range = |r: RangeRef| -> Option<RangeRef> {
|
||||
let e = if insert { Edit::Insert { axis, at, count } } else { Edit::Delete { axis, at, count } };
|
||||
let expr = gridcraft_formula::Expr::Ref(gridcraft_formula::Reference {
|
||||
sheet: gridcraft_formula::SheetSel::Current,
|
||||
kind: gridcraft_formula::RefKind::Range(
|
||||
gridcraft_formula::Anchor { row: r.start.row, col: r.start.col, row_abs: false, col_abs: false },
|
||||
gridcraft_formula::Anchor { row: r.end.row, col: r.end.col, row_abs: false, col_abs: false },
|
||||
),
|
||||
});
|
||||
match gridcraft_formula::adjust::adjust(expr, "S", "S", &e) {
|
||||
gridcraft_formula::Expr::Ref(r) => Some(r.range()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let e = if insert { Edit::Insert { axis, at, count } } else { Edit::Delete { axis, at, count } };
|
||||
let map_range = |r: RangeRef| map_range(r, &e);
|
||||
let map_cell = |c: CellRef| -> Option<CellRef> {
|
||||
match axis {
|
||||
Axis::Rows => map_line(c.row).map(|r| CellRef::new(r, c.col)),
|
||||
|
|
@ -265,6 +253,7 @@ fn insert_cells(s: &mut Session, p: &Json) -> Result<Json> {
|
|||
"column" | "entireColumn" => return insert_lines(s, &json!({"cols": RangeRef::cols(r.start.col, r.end.col).a1()}), Axis::Cols),
|
||||
_ => {}
|
||||
}
|
||||
let e = Edit::InsertCells { axis: if shift == "right" { Axis::Cols } else { Axis::Rows }, range: r };
|
||||
let sheet = target_sheet(s, p)?;
|
||||
let right = shift == "right";
|
||||
edit(s, |cx| {
|
||||
|
|
@ -274,15 +263,9 @@ fn insert_cells(s: &mut Session, p: &Json) -> Result<Json> {
|
|||
} else {
|
||||
sh.cells.shift_rows_in_cols(r.start.col, r.end.col, r.start.row, r.height() as i64);
|
||||
}
|
||||
// Formulas referring to cells that moved: approximate with a whole-sheet move edit.
|
||||
let moved = if right {
|
||||
RangeRef::new(r.start, CellRef::new(r.end.row, MAX_COLS - 1 - r.width()))
|
||||
} else {
|
||||
RangeRef::new(r.start, CellRef::new(MAX_ROWS - 1 - r.height(), r.end.col))
|
||||
};
|
||||
shift_cell_features(sh, &e);
|
||||
let name = cx.wb.sheet(sheet).map(|s| s.name.clone()).unwrap_or_default();
|
||||
let to = if right { (r.start.row, r.start.col + r.width()) } else { (r.start.row + r.height(), r.start.col) };
|
||||
super::edit::rewrite_all_formulas(&mut cx.wb, &name, &Edit::Move { from: moved, to_row: to.0, to_col: to.1 });
|
||||
rewrite(&mut cx.wb, &name, &e);
|
||||
cx.structural = true;
|
||||
Ok(Json::Null)
|
||||
})
|
||||
|
|
@ -296,6 +279,7 @@ fn delete_cells(s: &mut Session, p: &Json) -> Result<Json> {
|
|||
"column" | "entireColumn" => return delete_lines(s, &json!({"cols": RangeRef::cols(r.start.col, r.end.col).a1()}), Axis::Cols),
|
||||
_ => {}
|
||||
}
|
||||
let e = Edit::DeleteCells { axis: if shift == "left" { Axis::Cols } else { Axis::Rows }, range: r };
|
||||
let sheet = target_sheet(s, p)?;
|
||||
let left = shift == "left";
|
||||
edit(s, |cx| {
|
||||
|
|
@ -308,16 +292,10 @@ fn delete_cells(s: &mut Session, p: &Json) -> Result<Json> {
|
|||
} else {
|
||||
sh.cells.shift_rows_in_cols(r.start.col, r.end.col, r.end.row + 1, -(r.height() as i64));
|
||||
}
|
||||
shift_cell_features(sh, &e);
|
||||
}
|
||||
// References into the deleted block become #REF!; the rest move.
|
||||
let deleted = Edit::Move { from: r, to_row: r.start.row, to_col: r.start.col };
|
||||
let _ = deleted;
|
||||
let moved_from = if left {
|
||||
RangeRef::new(CellRef::new(r.start.row, r.end.col + 1), CellRef::new(r.end.row, MAX_COLS - 1))
|
||||
} else {
|
||||
RangeRef::new(CellRef::new(r.end.row + 1, r.start.col), CellRef::new(MAX_ROWS - 1, r.end.col))
|
||||
};
|
||||
super::edit::rewrite_all_formulas(&mut cx.wb, &name, &Edit::Move { from: moved_from, to_row: r.start.row, to_col: r.start.col });
|
||||
// References into the deleted cells become #REF!; the rest move.
|
||||
rewrite(&mut cx.wb, &name, &e);
|
||||
cx.structural = true;
|
||||
Ok(Json::Null)
|
||||
})
|
||||
|
|
@ -538,3 +516,53 @@ fn step_sheet(s: &mut Session, dir: i64) -> Result<Json> {
|
|||
s.doc_mut()?.wb_switch_sheet(i as usize);
|
||||
ok()
|
||||
}
|
||||
|
||||
/// Where a range of the edited sheet goes after a structural edit (`None` = deleted).
|
||||
fn map_range(r: RangeRef, e: &Edit) -> Option<RangeRef> {
|
||||
let expr = gridcraft_formula::Expr::Ref(gridcraft_formula::Reference {
|
||||
sheet: gridcraft_formula::SheetSel::Current,
|
||||
kind: gridcraft_formula::RefKind::Range(
|
||||
gridcraft_formula::Anchor { row: r.start.row, col: r.start.col, row_abs: false, col_abs: false },
|
||||
gridcraft_formula::Anchor { row: r.end.row, col: r.end.col, row_abs: false, col_abs: false },
|
||||
),
|
||||
});
|
||||
match gridcraft_formula::adjust::adjust(expr, "S", "S", e) {
|
||||
gridcraft_formula::Expr::Ref(r) => Some(r.range()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Shifts what Insert/Delete Cells moves along with the cells: merged areas, conditional
|
||||
/// format, validation, table, filter and print-area ranges, notes, links and sparklines.
|
||||
fn shift_cell_features(sh: &mut Sheet, e: &Edit) {
|
||||
let map_cell = |c: CellRef| map_range(RangeRef::cell(c), e).map(|r| r.start);
|
||||
sh.merges = sh.merges.iter().filter_map(|m| map_range(*m, e)).filter(|m| !m.is_single()).collect();
|
||||
for cf in sh.cond_formats.iter_mut() {
|
||||
cf.ranges = cf.ranges.iter().filter_map(|r| map_range(*r, e)).collect();
|
||||
}
|
||||
sh.cond_formats.retain(|cf| !cf.ranges.is_empty());
|
||||
for dv in sh.validations.iter_mut() {
|
||||
dv.ranges = dv.ranges.iter().filter_map(|r| map_range(*r, e)).collect();
|
||||
}
|
||||
sh.validations.retain(|dv| !dv.ranges.is_empty());
|
||||
for t in sh.tables.iter_mut() {
|
||||
t.range = map_range(t.range, e).unwrap_or_default();
|
||||
}
|
||||
sh.tables.retain(|t| t.range != RangeRef::default());
|
||||
if let Some(af) = sh.autofilter.as_mut() {
|
||||
match map_range(af.range, e) {
|
||||
Some(r) => af.range = r,
|
||||
None => sh.autofilter = None,
|
||||
}
|
||||
}
|
||||
sh.comments = std::mem::take(&mut sh.comments).into_iter().filter_map(|(c, v)| map_cell(c).map(|c| (c, v))).collect();
|
||||
sh.hyperlinks = std::mem::take(&mut sh.hyperlinks).into_iter().filter_map(|(c, v)| map_cell(c).map(|c| (c, v))).collect();
|
||||
for sp in sh.sparklines.iter_mut() {
|
||||
if let Some(c) = map_cell(sp.cell) {
|
||||
sp.cell = c;
|
||||
}
|
||||
}
|
||||
if let Some(pa) = sh.print.print_area {
|
||||
sh.print.print_area = map_range(pa, e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,53 @@ fn fill_series() {
|
|||
assert_eq!(v(&s, "C2"), Value::from("Item 10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_delete_cells_adjust() {
|
||||
let formula = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["formula"].clone();
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [3], [4], [5]]})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "C1", "input": "=SUM(A1:A5)"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "C2", "input": "=A4*10"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "C3", "input": "=SUM(A1:B5)"})).unwrap();
|
||||
s.execute("formulas.defineName", json!({"name": "Fourth", "refersTo": "=Sheet1!$A$4"})).unwrap();
|
||||
s.execute(
|
||||
"home.conditionalFormat",
|
||||
json!({"range": "A4:A5", "rule": {"type": "cellIs", "operator": "greater", "value": "3", "preset": "redText"}}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute("data.validation", json!({"range": "A5", "type": "whole", "operator": "greater", "formula1": "0"})).unwrap();
|
||||
s.execute("insert.chart", json!({"range": "A1:A5", "type": "line"})).unwrap();
|
||||
// Insert A3 shifting down: ranges across the insertion grow, references below move.
|
||||
s.execute("home.insertCells", json!({"range": "A3", "shift": "down"})).unwrap();
|
||||
assert_eq!(formula(&mut s, "C1"), "=SUM(A1:A6)");
|
||||
assert_eq!(formula(&mut s, "C2"), "=A5*10");
|
||||
assert_eq!(formula(&mut s, "C3"), "=SUM(A1:B5)");
|
||||
assert_eq!(v(&s, "C2"), Value::Number(40.0));
|
||||
let sh = s.doc().unwrap().wb.active().unwrap().clone();
|
||||
assert_eq!(s.doc().unwrap().wb.names[0].formula, "Sheet1!$A$5");
|
||||
assert_eq!(sh.cond_formats[0].ranges[0].a1(), "A5:A6");
|
||||
assert_eq!(sh.validations[0].ranges[0].a1(), "A6");
|
||||
assert_eq!(sh.charts[0].series[0].values, "Sheet1!$A$1:$A$6");
|
||||
// Delete A5 (the 4) shifting up: references to it become #REF!, those below move up.
|
||||
s.execute("home.deleteCells", json!({"range": "A5", "shift": "up"})).unwrap();
|
||||
assert_eq!(formula(&mut s, "C1"), "=SUM(A1:A5)");
|
||||
assert_eq!(formula(&mut s, "C2"), "=#REF!*10");
|
||||
assert_eq!(s.doc().unwrap().wb.names[0].formula, "#REF!");
|
||||
assert_eq!(v(&s, "C1"), Value::Number(11.0));
|
||||
let sh = s.doc().unwrap().wb.active().unwrap().clone();
|
||||
assert_eq!(sh.cond_formats[0].ranges[0].a1(), "A5");
|
||||
assert_eq!(sh.validations[0].ranges[0].a1(), "A5");
|
||||
assert_eq!(sh.charts[0].series[0].values, "Sheet1!$A$1:$A$5");
|
||||
// Shifting right and left.
|
||||
s.execute("range.setValues", json!({"range": "E1", "values": [[1, 2, 3]]})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "E3", "input": "=G1+SUM(E1:G1)"})).unwrap();
|
||||
s.execute("home.insertCells", json!({"range": "F1", "shift": "right"})).unwrap();
|
||||
assert_eq!(formula(&mut s, "E3"), "=H1+SUM(E1:H1)");
|
||||
s.execute("home.deleteCells", json!({"range": "E1:F1", "shift": "left"})).unwrap();
|
||||
assert_eq!(formula(&mut s, "E3"), "=F1+SUM(E1:F1)");
|
||||
assert_eq!(v(&s, "E3"), Value::Number(8.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_and_filter() {
|
||||
let mut s = s();
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@ pub enum Edit {
|
|||
Delete { axis: Axis, at: u32, count: u32 },
|
||||
/// Cut-paste of a block: references into `from` now point into the block at `to` (top-left).
|
||||
Move { from: RangeRef, to_row: u32, to_col: u32 },
|
||||
/// Insert Cells shifting down (`Rows`) or right (`Cols`): the cells of `range` and those
|
||||
/// below (right of) it move by its height (width), within its columns (rows) only.
|
||||
InsertCells { axis: Axis, range: RangeRef },
|
||||
/// Delete Cells shifting up (`Rows`) or left (`Cols`): `range` goes and the cells below
|
||||
/// (right of) it move back by its height (width), within its columns (rows) only.
|
||||
DeleteCells { axis: Axis, range: RangeRef },
|
||||
}
|
||||
|
||||
/// Adjusts a formula living on sheet `host` after `edit` on sheet `target`.
|
||||
|
|
@ -158,6 +164,21 @@ fn adjust_ref(r: &Reference, edit: &Edit) -> Option<Reference> {
|
|||
};
|
||||
Some(Reference { sheet: r.sheet.clone(), kind })
|
||||
}
|
||||
Edit::InsertCells { axis, range } | Edit::DeleteCells { axis, range } => {
|
||||
// Like inserting/deleting lines, for references that lie within the shifted
|
||||
// columns (rows); Excel leaves references reaching outside them as they are.
|
||||
let span = r.range();
|
||||
let (inside, at, count) = match axis {
|
||||
Axis::Rows => (range.start.col <= span.start.col && span.end.col <= range.end.col, range.start.row, range.height()),
|
||||
Axis::Cols => (range.start.row <= span.start.row && span.end.row <= range.end.row, range.start.col, range.width()),
|
||||
};
|
||||
if !inside {
|
||||
return Some(r.clone());
|
||||
}
|
||||
let axis = *axis;
|
||||
let lines = if matches!(edit, Edit::InsertCells { .. }) { Edit::Insert { axis, at, count } } else { Edit::Delete { axis, at, count } };
|
||||
adjust_ref(r, &lines)
|
||||
}
|
||||
Edit::Move { from, to_row, to_col } => {
|
||||
let dr = *to_row as i64 - from.start.row as i64;
|
||||
let dc = *to_col as i64 - from.start.col as i64;
|
||||
|
|
|
|||
|
|
@ -120,6 +120,24 @@ fn insert_delete() {
|
|||
assert_eq!(print(&adjust(e, "S", "S", &del)), "SUM(#REF!)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_delete_cells() {
|
||||
let range = |a: &str| gridcraft_core::RangeRef::parse(a).unwrap();
|
||||
let adj = |f: &str, e: &Edit| print(&adjust(parse(f).unwrap(), "S", "S", e));
|
||||
// Insert A3:A4, shifting down: column A moves, other columns and wider ranges don't.
|
||||
let ins = Edit::InsertCells { axis: Axis::Rows, range: range("A3:A4") };
|
||||
assert_eq!(adj("SUM(A1:A5)+A2+A3+$A$9+B3+SUM(A1:B5)+SUM(A:A)", &ins), "SUM(A1:A7)+A2+A5+$A$11+B3+SUM(A1:B5)+SUM(A:A)");
|
||||
// Insert B2 shifting right.
|
||||
let ins = Edit::InsertCells { axis: Axis::Cols, range: range("B2") };
|
||||
assert_eq!(adj("A2+B2+C2+B3+SUM(A2:C2)", &ins), "A2+C2+D2+B3+SUM(A2:D2)");
|
||||
// Delete A3:A4 shifting up: references into it become #REF!, those below move up.
|
||||
let del = Edit::DeleteCells { axis: Axis::Rows, range: range("A3:A4") };
|
||||
assert_eq!(adj("A2+A3+A4+A5+B5+SUM(A1:A10)+SUM(A3:A4)+SUM(A1:B10)", &del), "A2+#REF!+#REF!+A3+B5+SUM(A1:A8)+SUM(#REF!)+SUM(A1:B10)");
|
||||
// Delete B1:C2 shifting left.
|
||||
let del = Edit::DeleteCells { axis: Axis::Cols, range: range("B1:C2") };
|
||||
assert_eq!(adj("A1+B1+D1+D2+D3+SUM(A1:E1)", &del), "A1+#REF!+B1+B2+D3+SUM(A1:C1)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moves_and_renames() {
|
||||
let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("A1:B2").unwrap(), to_row: 9, to_col: 3 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue