gridcraft/crates/formula/src/adjust.rs

211 lines
7.8 KiB
Rust

//! Reference adjustment: copying formulas, inserting/deleting rows and columns, moving cells,
//! renaming and deleting sheets.
use gridcraft_core::{CellError, MAX_COLS, MAX_ROWS, RangeRef};
use crate::ast::*;
/// Copy/fill: relative parts move by `(dr, dc)`. References pushed off the sheet become `#REF!`.
pub fn shift_relative(e: Expr, dr: i64, dc: i64) -> Expr {
e.map(&mut |x| match x {
Expr::Ref(r) => match shift_ref(&r, dr, dc) {
Some(r) => Expr::Ref(r),
None => Expr::Error(CellError::Ref),
},
other => other,
})
}
fn shift_anchor(a: &Anchor, dr: i64, dc: i64) -> Option<Anchor> {
let row = if a.row_abs { a.row as i64 } else { a.row as i64 + dr };
let col = if a.col_abs { a.col as i64 } else { a.col as i64 + dc };
let (row, col) = clamp_anchor(row, col)?;
Some(Anchor { row, col, ..*a })
}
fn shift_line(v: u32, abs: bool, d: i64, max: u32) -> Option<u32> {
if abs {
return Some(v);
}
let n = v as i64 + d;
if (0..max as i64).contains(&n) { Some(n as u32) } else { None }
}
fn shift_ref(r: &Reference, dr: i64, dc: i64) -> Option<Reference> {
let kind = match &r.kind {
RefKind::Cell(a) => RefKind::Cell(shift_anchor(a, dr, dc)?),
RefKind::Range(a, b) => RefKind::Range(shift_anchor(a, dr, dc)?, shift_anchor(b, dr, dc)?),
RefKind::Rows(r0, a0, r1, a1) => RefKind::Rows(shift_line(*r0, *a0, dr, MAX_ROWS)?, *a0, shift_line(*r1, *a1, dr, MAX_ROWS)?, *a1),
RefKind::Cols(c0, a0, c1, a1) => RefKind::Cols(shift_line(*c0, *a0, dc, MAX_COLS)?, *a0, shift_line(*c1, *a1, dc, MAX_COLS)?, *a1),
};
Some(Reference { sheet: r.sheet.clone(), kind })
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Axis {
Rows,
Cols,
}
/// A structural edit on one sheet.
#[derive(Clone, Debug, PartialEq)]
pub enum Edit {
/// Insert `count` rows/cols before index `at`.
Insert { axis: Axis, at: u32, count: u32 },
/// Delete `count` rows/cols starting at `at`.
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 },
}
/// Adjusts a formula living on sheet `host` after `edit` on sheet `target`.
pub fn adjust(e: Expr, host: &str, target: &str, edit: &Edit) -> Expr {
e.map(&mut |x| match x {
Expr::Ref(r) => {
let on_target = match &r.sheet {
SheetSel::Current => host.eq_ignore_ascii_case(target),
SheetSel::Named(n) => n.eq_ignore_ascii_case(target),
SheetSel::Span(..) => false,
};
if !on_target {
return Expr::Ref(r);
}
match adjust_ref(&r, edit) {
Some(r) => Expr::Ref(r),
None => Expr::Error(CellError::Ref),
}
}
other => other,
})
}
/// New position of an index after an insert (`None` never) or delete (`None` = deleted).
fn map_index(v: u32, at: u32, count: u32, insert: bool, max: u32) -> Option<u32> {
if insert {
if v >= at {
let n = v as u64 + count as u64;
if n >= max as u64 { None } else { Some(n as u32) }
} else {
Some(v)
}
} else if v < at {
Some(v)
} else if v >= at.saturating_add(count) {
Some(v - count)
} else {
None
}
}
/// Adjusts a span `[lo, hi]` (inclusive) on one axis.
fn map_span(lo: u32, hi: u32, at: u32, count: u32, insert: bool, max: u32) -> Option<(u32, u32)> {
if insert {
let lo2 = if lo >= at { lo as u64 + count as u64 } else { lo as u64 };
let hi2 = if hi >= at { hi as u64 + count as u64 } else { hi as u64 };
// Whole-sheet spans (e.g. A:A) stay whole.
let hi2 = hi2.min(max as u64 - 1);
if lo2 > hi2 {
return None;
}
Some((lo2 as u32, hi2 as u32))
} else {
let end = at.saturating_add(count); // exclusive
if lo >= at && hi < end {
return None; // fully deleted
}
let lo2 = if lo < at {
lo
} else if lo >= end {
lo - count
} else {
at
};
let hi2 = if hi < at {
hi
} else if hi >= end {
hi - count
} else {
at.saturating_sub(1)
};
Some((lo2, hi2.max(lo2)))
}
}
fn adjust_ref(r: &Reference, edit: &Edit) -> Option<Reference> {
match edit {
Edit::Insert { axis, at, count } | Edit::Delete { axis, at, count } => {
let insert = matches!(edit, Edit::Insert { .. });
let kind = match (&r.kind, axis) {
(RefKind::Cell(a), Axis::Rows) => RefKind::Cell(Anchor { row: map_index(a.row, *at, *count, insert, MAX_ROWS)?, ..*a }),
(RefKind::Cell(a), Axis::Cols) => RefKind::Cell(Anchor { col: map_index(a.col, *at, *count, insert, MAX_COLS)?, ..*a }),
(RefKind::Range(a, b), Axis::Rows) => {
let (lo, hi) = map_span(a.row.min(b.row), a.row.max(b.row), *at, *count, insert, MAX_ROWS)?;
RefKind::Range(Anchor { row: lo, ..*a }, Anchor { row: hi, ..*b })
}
(RefKind::Range(a, b), Axis::Cols) => {
let (lo, hi) = map_span(a.col.min(b.col), a.col.max(b.col), *at, *count, insert, MAX_COLS)?;
RefKind::Range(Anchor { col: lo, ..*a }, Anchor { col: hi, ..*b })
}
(RefKind::Rows(r0, a0, r1, a1), Axis::Rows) => {
let (lo, hi) = map_span(*r0.min(r1), *r0.max(r1), *at, *count, insert, MAX_ROWS)?;
RefKind::Rows(lo, *a0, hi, *a1)
}
(RefKind::Cols(c0, a0, c1, a1), Axis::Cols) => {
let (lo, hi) = map_span(*c0.min(c1), *c0.max(c1), *at, *count, insert, MAX_COLS)?;
RefKind::Cols(lo, *a0, hi, *a1)
}
(k, _) => k.clone(),
};
Some(Reference { sheet: r.sheet.clone(), kind })
}
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;
let range = r.range();
if !from.contains_range(&range) {
return Some(r.clone());
}
let mv = |a: &Anchor| -> Option<Anchor> {
let (row, col) = clamp_anchor(a.row as i64 + dr, a.col as i64 + dc)?;
Some(Anchor { row, col, ..*a })
};
let kind = match &r.kind {
RefKind::Cell(a) => RefKind::Cell(mv(a)?),
RefKind::Range(a, b) => RefKind::Range(mv(a)?, mv(b)?),
k => k.clone(),
};
Some(Reference { sheet: r.sheet.clone(), kind })
}
}
}
/// Renames sheet references `old` → `new`.
pub fn rename_sheet(e: Expr, old: &str, new: &str) -> Expr {
e.map(&mut |x| match x {
Expr::Ref(mut r) => {
match &mut r.sheet {
SheetSel::Named(n) if n.eq_ignore_ascii_case(old) => *n = new.to_string(),
SheetSel::Span(a, b) => {
if a.eq_ignore_ascii_case(old) {
*a = new.to_string();
}
if b.eq_ignore_ascii_case(old) {
*b = new.to_string();
}
}
_ => {}
}
Expr::Ref(r)
}
other => other,
})
}
/// References to a deleted sheet become `#REF!`.
pub fn delete_sheet(e: Expr, name: &str) -> Expr {
e.map(&mut |x| match x {
Expr::Ref(r) if r.sheet_name().is_some_and(|n| n.eq_ignore_ascii_case(name)) => Expr::Error(CellError::Ref),
other => other,
})
}