use crate::core::formula::{CompiledFormula, FormulaPart};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Axis {
Row,
Col,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct GridEdit {
pub sheet_id: u64,
pub axis: Axis,
pub at: usize,
pub count: usize,
pub insert: bool,
pub band: Option<(usize, usize)>,
}
impl GridEdit {
pub fn insert_row(sheet_id: u64, at: usize) -> Self {
Self {
sheet_id,
axis: Axis::Row,
at,
count: 1,
insert: true,
band: None,
}
}
pub fn delete_row(sheet_id: u64, at: usize) -> Self {
Self {
sheet_id,
axis: Axis::Row,
at,
count: 1,
insert: false,
band: None,
}
}
pub fn insert_col(sheet_id: u64, at: usize) -> Self {
Self {
sheet_id,
axis: Axis::Col,
at,
count: 1,
insert: true,
band: None,
}
}
pub fn delete_col(sheet_id: u64, at: usize) -> Self {
Self {
sheet_id,
axis: Axis::Col,
at,
count: 1,
insert: false,
band: None,
}
}
pub fn band_rows(
sheet_id: u64,
at: usize,
count: usize,
first_col: usize,
last_col: usize,
insert: bool,
) -> Self {
Self {
sheet_id,
axis: Axis::Row,
at,
count,
insert,
band: Some((first_col, last_col)),
}
}
pub(crate) fn covers_columns(&self, first_col: usize, last_col: usize) -> bool {
match self.band {
None => true,
Some((lo, hi)) => lo <= first_col && last_col <= hi,
}
}
fn point(&self, index: usize) -> Option<usize> {
shift_point(index, self.at, self.count, self.insert)
}
fn span(&self, start: usize, end: usize) -> Option<(usize, usize)> {
shift_span(start, end, self.at, self.count, self.insert)
}
}
pub(crate) fn shift_point(index: usize, at: usize, count: usize, insert: bool) -> Option<usize> {
if insert {
Some(if index >= at { index + count } else { index })
} else if index < at {
Some(index)
} else if index < at + count {
None
} else {
Some(index - count)
}
}
pub(crate) fn shift_span(
start: usize,
end: usize,
at: usize,
count: usize,
insert: bool,
) -> Option<(usize, usize)> {
debug_assert!(end < usize::MAX, "unbounded span reached shift_span");
if insert {
let new_start = if start >= at { start + count } else { start };
let new_end = if end >= at { end + count } else { end };
return Some((new_start, new_end));
}
let removed_below = |bound: usize| count.min(bound.saturating_sub(at));
let new_start = start - removed_below(start);
let new_end_exclusive = (end + 1) - removed_below(end + 1);
if new_end_exclusive <= new_start {
None
} else {
Some((new_start, new_end_exclusive - 1))
}
}
pub(crate) fn shift_rect(
edit: &GridEdit,
start_row: usize,
start_col: usize,
end_row: usize,
end_col: usize,
) -> Option<(usize, usize, usize, usize)> {
match edit.axis {
Axis::Row => {
let (r0, r1) = edit.span(start_row, end_row)?;
Some((r0, start_col, r1, end_col))
}
Axis::Col => {
let (c0, c1) = edit.span(start_col, end_col)?;
Some((start_row, c0, end_row, c1))
}
}
}
pub(crate) fn shift_formula(
formula: &CompiledFormula,
edit: &GridEdit,
deleted_col_ids: &[u64],
) -> Option<CompiledFormula> {
let mut changed = false;
let parts = formula
.parts
.iter()
.map(|part| {
let (next, part_changed) = shift_part(part, edit, deleted_col_ids);
changed |= part_changed;
next
})
.collect();
changed.then_some(CompiledFormula { parts })
}
const REF_ERROR: &str = "#REF!";
fn shift_part(part: &FormulaPart, edit: &GridEdit, deleted_col_ids: &[u64]) -> (FormulaPart, bool) {
let unchanged = || (part.clone(), false);
let broken = || (FormulaPart::Text(REF_ERROR.to_string()), true);
match part {
FormulaPart::Text(_) | FormulaPart::StructuredReference { .. } => unchanged(),
FormulaPart::ColumnReference { sheet_id, col_id } => {
if *sheet_id != edit.sheet_id {
unchanged()
} else if deleted_col_ids.contains(col_id) {
broken()
} else {
unchanged()
}
}
FormulaPart::SheetReference {
sheet_id,
row,
col,
row_ref_type,
col_ref_type,
} => {
if *sheet_id != edit.sheet_id || !edit.covers_columns(*col, *col) {
return unchanged();
}
let (new_row, new_col) = match edit.axis {
Axis::Row => match edit.point(*row) {
Some(r) => (r, *col),
None => return broken(),
},
Axis::Col => match edit.point(*col) {
Some(c) => (*row, c),
None => return broken(),
},
};
if (new_row, new_col) == (*row, *col) {
return unchanged();
}
(
FormulaPart::SheetReference {
sheet_id: *sheet_id,
row: new_row,
col: new_col,
row_ref_type: *row_ref_type,
col_ref_type: *col_ref_type,
},
true,
)
}
FormulaPart::RangeReference {
sheet_id,
start_row,
start_col,
end_row,
end_col,
start_row_ref_type,
start_col_ref_type,
end_row_ref_type,
end_col_ref_type,
} => {
if *sheet_id != edit.sheet_id || !edit.covers_columns(*start_col, *end_col) {
return unchanged();
}
if *end_row == usize::MAX && edit.axis == Axis::Row {
return unchanged();
}
let rect = match shift_rect(edit, *start_row, *start_col, *end_row, *end_col) {
Some(rect) => rect,
None => return broken(),
};
if rect == (*start_row, *start_col, *end_row, *end_col) {
return unchanged();
}
let (r0, c0, r1, c1) = rect;
(
FormulaPart::RangeReference {
sheet_id: *sheet_id,
start_row: r0,
start_col: c0,
end_row: r1,
end_col: c1,
start_row_ref_type: *start_row_ref_type,
start_col_ref_type: *start_col_ref_type,
end_row_ref_type: *end_row_ref_type,
end_col_ref_type: *end_col_ref_type,
},
true,
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::RefType;
#[test]
fn an_insert_moves_what_is_at_or_below_it() {
assert_eq!(shift_point(0, 1, 1, true), Some(0));
assert_eq!(shift_point(1, 1, 1, true), Some(2));
assert_eq!(shift_point(5, 1, 3, true), Some(8));
}
#[test]
fn a_delete_removes_its_own_indices_and_pulls_the_rest_up() {
assert_eq!(shift_point(0, 1, 1, false), Some(0));
assert_eq!(shift_point(1, 1, 1, false), None);
assert_eq!(shift_point(2, 1, 1, false), Some(1));
assert_eq!(shift_point(4, 1, 3, false), Some(1));
assert_eq!(shift_point(3, 1, 3, false), None);
}
#[test]
fn inserting_at_a_spans_start_moves_it_and_inserting_inside_grows_it() {
assert_eq!(shift_span(1, 3, 1, 1, true), Some((2, 4)));
assert_eq!(shift_span(1, 3, 2, 1, true), Some((1, 4)));
assert_eq!(shift_span(1, 3, 4, 1, true), Some((1, 3)));
}
#[test]
fn deleting_inside_a_span_shrinks_it() {
assert_eq!(shift_span(1, 3, 2, 1, false), Some((1, 2)));
assert_eq!(shift_span(1, 3, 1, 1, false), Some((1, 2)));
assert_eq!(shift_span(1, 3, 0, 1, false), Some((0, 2)));
assert_eq!(shift_span(1, 3, 4, 1, false), Some((1, 3)));
}
#[test]
fn a_span_deleted_in_full_is_gone() {
assert_eq!(shift_span(2, 2, 2, 1, false), None);
assert_eq!(shift_span(2, 4, 1, 5, false), None);
assert_eq!(shift_span(2, 4, 3, 5, false), Some((2, 2)));
}
#[test]
fn a_dollar_sign_does_not_pin_a_reference_against_a_structural_edit() {
let formula = CompiledFormula {
parts: vec![
FormulaPart::Text("=".to_string()),
FormulaPart::SheetReference {
sheet_id: 1,
row: 2,
col: 0,
row_ref_type: RefType::Absolute,
col_ref_type: RefType::Absolute,
},
],
};
let shifted = shift_formula(&formula, &GridEdit::insert_row(1, 0), &[]).unwrap();
assert_eq!(
shifted.parts[1],
FormulaPart::SheetReference {
sheet_id: 1,
row: 3,
col: 0,
row_ref_type: RefType::Absolute,
col_ref_type: RefType::Absolute,
}
);
}
#[test]
fn an_edit_on_another_sheet_leaves_a_reference_alone() {
let formula = CompiledFormula {
parts: vec![FormulaPart::SheetReference {
sheet_id: 1,
row: 2,
col: 0,
row_ref_type: RefType::Relative,
col_ref_type: RefType::Relative,
}],
};
assert!(shift_formula(&formula, &GridEdit::insert_row(2, 0), &[]).is_none());
}
#[test]
fn only_the_deleted_reference_becomes_ref_error() {
let formula = CompiledFormula {
parts: vec![
FormulaPart::Text("=".to_string()),
FormulaPart::SheetReference {
sheet_id: 1,
row: 2,
col: 0,
row_ref_type: RefType::Relative,
col_ref_type: RefType::Relative,
},
FormulaPart::Text("+1".to_string()),
],
};
let shifted = shift_formula(&formula, &GridEdit::delete_row(1, 2), &[]).unwrap();
assert_eq!(
shifted.parts,
vec![
FormulaPart::Text("=".to_string()),
FormulaPart::Text("#REF!".to_string()),
FormulaPart::Text("+1".to_string()),
]
);
}
#[test]
fn a_whole_column_reference_survives_a_move_and_breaks_on_its_own_deletion() {
let formula = CompiledFormula {
parts: vec![FormulaPart::ColumnReference {
sheet_id: 1,
col_id: 7,
}],
};
assert!(shift_formula(&formula, &GridEdit::insert_col(1, 0), &[]).is_none());
assert!(shift_formula(&formula, &GridEdit::delete_col(1, 0), &[9]).is_none());
let shifted = shift_formula(&formula, &GridEdit::delete_col(1, 0), &[7]).unwrap();
assert_eq!(shifted.parts, vec![FormulaPart::Text("#REF!".to_string())]);
}
#[test]
fn an_unbounded_row_range_survives_a_row_edit_and_still_tracks_columns() {
let unbounded = |start_col, end_col| CompiledFormula {
parts: vec![FormulaPart::RangeReference {
sheet_id: 1,
start_row: 0,
start_col,
end_row: usize::MAX,
end_col,
start_row_ref_type: RefType::Absolute,
start_col_ref_type: RefType::Relative,
end_row_ref_type: RefType::Absolute,
end_col_ref_type: RefType::Relative,
}],
};
assert!(shift_formula(&unbounded(0, 2), &GridEdit::delete_row(1, 0), &[]).is_none());
assert!(shift_formula(&unbounded(0, 2), &GridEdit::insert_row(1, 0), &[]).is_none());
let shifted = shift_formula(&unbounded(0, 2), &GridEdit::insert_col(1, 0), &[]).unwrap();
assert_eq!(shifted.parts, unbounded(1, 3).parts);
}
#[test]
fn a_row_edit_leaves_columns_alone_and_a_column_edit_leaves_rows_alone() {
let rect = (2, 2, 4, 4);
let (r0, c0, r1, c1) =
shift_rect(&GridEdit::insert_row(1, 0), rect.0, rect.1, rect.2, rect.3).unwrap();
assert_eq!((r0, c0, r1, c1), (3, 2, 5, 4));
let (r0, c0, r1, c1) =
shift_rect(&GridEdit::insert_col(1, 0), rect.0, rect.1, rect.2, rect.3).unwrap();
assert_eq!((r0, c0, r1, c1), (2, 3, 4, 5));
}
#[test]
fn a_band_edit_moves_only_references_wholly_inside_the_band() {
let edit = GridEdit::band_rows(1, 1, 1, 0, 2, true);
let cell = |row, col| CompiledFormula {
parts: vec![FormulaPart::SheetReference {
sheet_id: 1,
row,
col,
row_ref_type: RefType::Relative,
col_ref_type: RefType::Relative,
}],
};
assert_eq!(
shift_formula(&cell(4, 0), &edit, &[]).unwrap().parts,
cell(5, 0).parts
);
assert_eq!(
shift_formula(&cell(1, 0), &edit, &[]).unwrap().parts,
cell(2, 0).parts
);
assert!(shift_formula(&cell(0, 0), &edit, &[]).is_none());
assert!(shift_formula(&cell(4, 4), &edit, &[]).is_none());
}
#[test]
fn a_range_straddling_the_bands_edge_does_not_move_at_all() {
let edit = GridEdit::band_rows(1, 1, 1, 0, 2, true);
let range = |start_col, end_col| CompiledFormula {
parts: vec![FormulaPart::RangeReference {
sheet_id: 1,
start_row: 4,
start_col,
end_row: 5,
end_col,
start_row_ref_type: RefType::Relative,
start_col_ref_type: RefType::Relative,
end_row_ref_type: RefType::Relative,
end_col_ref_type: RefType::Relative,
}],
};
assert!(shift_formula(&range(0, 4), &edit, &[]).is_none());
let moved = shift_formula(&range(0, 2), &edit, &[]).unwrap();
let FormulaPart::RangeReference {
start_row, end_row, ..
} = moved.parts[0]
else {
panic!("expected a range");
};
assert_eq!((start_row, end_row), (5, 6));
assert!(shift_formula(&range(4, 5), &edit, &[]).is_none());
}
#[test]
fn a_band_edit_grows_a_range_that_spans_its_insert_point() {
let edit = GridEdit::band_rows(1, 1, 1, 0, 2, true);
let formula = CompiledFormula {
parts: vec![FormulaPart::RangeReference {
sheet_id: 1,
start_row: 0,
start_col: 0,
end_row: 5,
end_col: 0,
start_row_ref_type: RefType::Relative,
start_col_ref_type: RefType::Relative,
end_row_ref_type: RefType::Relative,
end_col_ref_type: RefType::Relative,
}],
};
let grown = shift_formula(&formula, &edit, &[]).unwrap();
let FormulaPart::RangeReference {
start_row, end_row, ..
} = grown.parts[0]
else {
panic!("expected a range");
};
assert_eq!((start_row, end_row), (0, 6));
}
#[test]
fn a_whole_column_reference_ignores_a_band_edit() {
let edit = GridEdit::band_rows(1, 1, 1, 0, 2, true);
let whole = CompiledFormula {
parts: vec![FormulaPart::ColumnReference {
sheet_id: 1,
col_id: 7,
}],
};
assert!(shift_formula(&whole, &edit, &[]).is_none());
}
}