use crate::reference::{CellRef, Coord};
use crate::{SheetId, engine::sheet_registry::SheetRegistry};
use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AbsShiftPolicy {
Track,
Pin,
}
impl AbsShiftPolicy {
#[inline]
fn pins(self) -> bool {
matches!(self, Self::Pin)
}
}
pub struct ReferenceAdjuster;
pub(crate) struct ReferenceContext<'a> {
formula_sheet_id: SheetId,
sheet_registry: &'a SheetRegistry,
}
impl<'a> ReferenceContext<'a> {
pub(crate) fn new(formula_sheet_id: SheetId, sheet_registry: &'a SheetRegistry) -> Self {
Self {
formula_sheet_id,
sheet_registry,
}
}
fn reference_sheet_id(&self, sheet: Option<&str>) -> Option<SheetId> {
match sheet {
Some(name) => self.sheet_registry.get_id(name),
None => Some(self.formula_sheet_id),
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum ReferenceAdjustment {
Reference(ReferenceType),
Invalidated,
}
#[derive(Debug, Clone)]
pub enum ShiftOperation {
InsertRows {
sheet_id: u16,
before: u32,
count: u32,
},
DeleteRows {
sheet_id: u16,
start: u32,
count: u32,
},
InsertColumns {
sheet_id: u16,
before: u32,
count: u32,
},
DeleteColumns {
sheet_id: u16,
start: u32,
count: u32,
},
}
impl ReferenceAdjuster {
pub fn new() -> Self {
Self
}
pub fn adjust_ast(&self, ast: &ASTNode, op: &ShiftOperation) -> ASTNode {
self.adjust_ast_with_policy(ast, op, AbsShiftPolicy::Track)
}
pub(crate) fn adjust_ast_with_policy(
&self,
ast: &ASTNode,
op: &ShiftOperation,
policy: AbsShiftPolicy,
) -> ASTNode {
self.adjust_ast_if_changed_inner(ast, op, policy, None)
.unwrap_or_else(|| ast.clone())
}
pub fn adjust_ast_if_changed(&self, ast: &ASTNode, op: &ShiftOperation) -> Option<ASTNode> {
self.adjust_ast_if_changed_with_policy(ast, op, AbsShiftPolicy::Track)
}
pub(crate) fn adjust_ast_if_changed_with_policy(
&self,
ast: &ASTNode,
op: &ShiftOperation,
policy: AbsShiftPolicy,
) -> Option<ASTNode> {
self.adjust_ast_if_changed_inner(ast, op, policy, None)
}
pub(crate) fn adjust_ast_in_context(
&self,
ast: &ASTNode,
op: &ShiftOperation,
context: &ReferenceContext<'_>,
) -> ASTNode {
self.adjust_ast_with_policy_in_context(ast, op, AbsShiftPolicy::Track, context)
}
pub(crate) fn adjust_ast_with_policy_in_context(
&self,
ast: &ASTNode,
op: &ShiftOperation,
policy: AbsShiftPolicy,
context: &ReferenceContext<'_>,
) -> ASTNode {
self.adjust_ast_if_changed_inner(ast, op, policy, Some(context))
.unwrap_or_else(|| ast.clone())
}
pub(crate) fn adjust_ast_if_changed_in_context(
&self,
ast: &ASTNode,
op: &ShiftOperation,
context: &ReferenceContext<'_>,
) -> Option<ASTNode> {
self.adjust_ast_if_changed_with_policy_in_context(ast, op, AbsShiftPolicy::Track, context)
}
pub(crate) fn adjust_ast_if_changed_with_policy_in_context(
&self,
ast: &ASTNode,
op: &ShiftOperation,
policy: AbsShiftPolicy,
context: &ReferenceContext<'_>,
) -> Option<ASTNode> {
self.adjust_ast_if_changed_inner(ast, op, policy, Some(context))
}
fn adjust_ast_if_changed_inner(
&self,
ast: &ASTNode,
op: &ShiftOperation,
policy: AbsShiftPolicy,
context: Option<&ReferenceContext<'_>>,
) -> Option<ASTNode> {
let changed_node_type = match &ast.node_type {
ASTNodeType::Reference { reference, .. } => {
match self.adjust_reference(reference, op, policy, context) {
ReferenceAdjustment::Reference(adjusted) if adjusted == *reference => {
return None;
}
ReferenceAdjustment::Reference(adjusted) => ASTNodeType::Reference {
original: adjusted.normalise(),
reference: adjusted,
},
ReferenceAdjustment::Invalidated => ASTNodeType::Literal(LiteralValue::Error(
ExcelError::new(ExcelErrorKind::Ref),
)),
}
}
ASTNodeType::BinaryOp {
op: bin_op,
left,
right,
} => {
let adjusted_left = self.adjust_ast_if_changed_inner(left, op, policy, context);
let adjusted_right = self.adjust_ast_if_changed_inner(right, op, policy, context);
if adjusted_left.is_none() && adjusted_right.is_none() {
return None;
}
ASTNodeType::BinaryOp {
op: bin_op.clone(),
left: Box::new(adjusted_left.unwrap_or_else(|| (**left).clone())),
right: Box::new(adjusted_right.unwrap_or_else(|| (**right).clone())),
}
}
ASTNodeType::UnaryOp { op: un_op, expr } => ASTNodeType::UnaryOp {
op: un_op.clone(),
expr: Box::new(self.adjust_ast_if_changed_inner(expr, op, policy, context)?),
},
ASTNodeType::Function { name, args } => {
let (args, changed) = self.adjust_children(args, op, policy, context);
if !changed {
return None;
}
ASTNodeType::Function {
name: name.clone(),
args,
}
}
ASTNodeType::Call { callee, args } => {
let adjusted_callee = self.adjust_ast_if_changed_inner(callee, op, policy, context);
let (args, args_changed) = self.adjust_children(args, op, policy, context);
if adjusted_callee.is_none() && !args_changed {
return None;
}
ASTNodeType::Call {
callee: Box::new(adjusted_callee.unwrap_or_else(|| (**callee).clone())),
args,
}
}
ASTNodeType::Array(rows) => {
let mut changed = false;
let rows = rows
.iter()
.map(|row| {
let (row, row_changed) = self.adjust_children(row, op, policy, context);
changed |= row_changed;
row
})
.collect();
if !changed {
return None;
}
ASTNodeType::Array(rows)
}
_ => return None,
};
Some(ASTNode {
node_type: changed_node_type,
source_token: None,
contains_volatile: ast.contains_volatile,
})
}
fn adjust_children(
&self,
children: &[ASTNode],
op: &ShiftOperation,
policy: AbsShiftPolicy,
context: Option<&ReferenceContext<'_>>,
) -> (Vec<ASTNode>, bool) {
let mut changed = false;
let children = children
.iter()
.map(|child| {
if let Some(adjusted) = self.adjust_ast_if_changed_inner(child, op, policy, context)
{
changed = true;
adjusted
} else {
child.clone()
}
})
.collect();
(children, changed)
}
pub fn adjust_cell_ref(&self, cell_ref: &CellRef, op: &ShiftOperation) -> Option<CellRef> {
self.adjust_cell_ref_with_policy(cell_ref, op, AbsShiftPolicy::Track)
}
pub(crate) fn adjust_cell_ref_with_policy(
&self,
cell_ref: &CellRef,
op: &ShiftOperation,
policy: AbsShiftPolicy,
) -> Option<CellRef> {
let coord = cell_ref.coord;
let adjusted_coord = match op {
ShiftOperation::InsertRows {
sheet_id,
before,
count,
} if cell_ref.sheet_id == *sheet_id => {
if (policy.pins() && coord.row_abs()) || coord.row() < *before {
coord
} else {
Coord::new(
coord.row() + count,
coord.col(),
coord.row_abs(),
coord.col_abs(),
)
}
}
ShiftOperation::DeleteRows {
sheet_id,
start,
count,
} if cell_ref.sheet_id == *sheet_id => {
if policy.pins() && coord.row_abs() {
coord
} else if coord.row() >= *start && coord.row() < start + count {
return None;
} else if coord.row() >= start + count {
Coord::new(
coord.row() - count,
coord.col(),
coord.row_abs(),
coord.col_abs(),
)
} else {
coord
}
}
ShiftOperation::InsertColumns {
sheet_id,
before,
count,
} if cell_ref.sheet_id == *sheet_id => {
if (policy.pins() && coord.col_abs()) || coord.col() < *before {
coord
} else {
Coord::new(
coord.row(),
coord.col() + count,
coord.row_abs(),
coord.col_abs(),
)
}
}
ShiftOperation::DeleteColumns {
sheet_id,
start,
count,
} if cell_ref.sheet_id == *sheet_id => {
if policy.pins() && coord.col_abs() {
coord
} else if coord.col() >= *start && coord.col() < start + count {
return None;
} else if coord.col() >= start + count {
Coord::new(
coord.row(),
coord.col() - count,
coord.row_abs(),
coord.col_abs(),
)
} else {
coord
}
}
_ => coord,
};
Some(CellRef::new(cell_ref.sheet_id, adjusted_coord))
}
fn adjust_reference(
&self,
reference: &ReferenceType,
op: &ShiftOperation,
policy: AbsShiftPolicy,
context: Option<&ReferenceContext<'_>>,
) -> ReferenceAdjustment {
let op_sheet_id = match op {
ShiftOperation::InsertRows { sheet_id, .. }
| ShiftOperation::DeleteRows { sheet_id, .. }
| ShiftOperation::InsertColumns { sheet_id, .. }
| ShiftOperation::DeleteColumns { sheet_id, .. } => *sheet_id,
};
match reference {
ReferenceType::Cell { sheet, .. } | ReferenceType::Range { sheet, .. } => {
if context.is_some_and(|context| {
context.reference_sheet_id(sheet.as_deref()) != Some(op_sheet_id)
}) {
return ReferenceAdjustment::Reference(reference.clone());
}
}
_ => return ReferenceAdjustment::Reference(reference.clone()),
}
let shared = reference.to_sheet_ref_lossy();
match (reference, shared) {
(
ReferenceType::Cell {
sheet,
row_abs,
col_abs,
..
},
Some(crate::reference::SharedRef::Cell(cell)),
) => {
let temp_ref = CellRef::new(
op_sheet_id,
Coord::new(cell.coord.row(), cell.coord.col(), *row_abs, *col_abs),
);
match self.adjust_cell_ref_with_policy(&temp_ref, op, policy) {
None => ReferenceAdjustment::Invalidated,
Some(adjusted) => ReferenceAdjustment::Reference(ReferenceType::Cell {
sheet: sheet.clone(),
row: adjusted.coord.row() + 1,
col: adjusted.coord.col() + 1,
row_abs: *row_abs,
col_abs: *col_abs,
}),
}
}
(
ReferenceType::Range {
sheet,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
..
},
Some(crate::reference::SharedRef::Range(range)),
) => {
let sr = range.start_row;
let sc = range.start_col;
let er = range.end_row;
let ec = range.end_col;
let adjust_insert = |b: formualizer_common::AxisBound, before: u32, count: u32| {
if policy.pins() && b.abs {
b.index
} else if b.index >= before {
b.index + count
} else {
b.index
}
};
let adjust_delete = |idx: u32, abs: bool, start: u32, count: u32| {
if policy.pins() && abs {
idx
} else if idx >= start + count {
idx - count
} else if idx >= start {
start
} else {
idx
}
};
let (adj_sr0, adj_er0) = match op {
ShiftOperation::InsertRows { before, count, .. } => (
sr.map(|b| adjust_insert(b, *before, *count)),
er.map(|b| adjust_insert(b, *before, *count)),
),
ShiftOperation::DeleteRows { start, count, .. } => match (sr, er) {
(Some(range_start), Some(range_end))
if !policy.pins() || (!range_start.abs && !range_end.abs) =>
{
let range_start = range_start.index;
let range_end = range_end.index;
if range_end < *start || range_start >= start + count {
let adj_start = if range_start >= start + count {
range_start - count
} else {
range_start
};
let adj_end = if range_end >= start + count {
range_end - count
} else {
range_end
};
(Some(adj_start), Some(adj_end))
} else if range_start >= *start && range_end < start + count {
return ReferenceAdjustment::Invalidated;
} else {
let adj_start = if range_start < *start {
range_start
} else {
*start
};
let adj_end = if range_end >= start + count {
range_end - count
} else {
start.saturating_sub(1)
};
(Some(adj_start), Some(adj_end))
}
}
(Some(range_start), Some(range_end)) => {
let adj_start =
adjust_delete(range_start.index, range_start.abs, *start, *count);
let adj_end =
adjust_delete(range_end.index, range_end.abs, *start, *count);
(Some(adj_start), Some(adj_end))
}
_ => (
sr.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
er.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
),
},
_ => (sr.map(|b| b.index), er.map(|b| b.index)),
};
let (adj_sc0, adj_ec0) = match op {
ShiftOperation::InsertColumns { before, count, .. } => (
sc.map(|b| adjust_insert(b, *before, *count)),
ec.map(|b| adjust_insert(b, *before, *count)),
),
ShiftOperation::DeleteColumns { start, count, .. } => match (sc, ec) {
(Some(range_start), Some(range_end))
if !policy.pins() || (!range_start.abs && !range_end.abs) =>
{
let range_start = range_start.index;
let range_end = range_end.index;
if range_end < *start || range_start >= start + count {
let adj_start = if range_start >= start + count {
range_start - count
} else {
range_start
};
let adj_end = if range_end >= start + count {
range_end - count
} else {
range_end
};
(Some(adj_start), Some(adj_end))
} else if range_start >= *start && range_end < start + count {
return ReferenceAdjustment::Invalidated;
} else {
let adj_start = if range_start < *start {
range_start
} else {
*start
};
let adj_end = if range_end >= start + count {
range_end - count
} else {
start.saturating_sub(1)
};
(Some(adj_start), Some(adj_end))
}
}
(Some(range_start), Some(range_end)) => {
let adj_start =
adjust_delete(range_start.index, range_start.abs, *start, *count);
let adj_end =
adjust_delete(range_end.index, range_end.abs, *start, *count);
(Some(adj_start), Some(adj_end))
}
_ => (
sc.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
ec.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
),
},
_ => (sc.map(|b| b.index), ec.map(|b| b.index)),
};
ReferenceAdjustment::Reference(ReferenceType::Range {
sheet: sheet.clone(),
start_row: adj_sr0.map(|i| i + 1),
start_col: adj_sc0.map(|i| i + 1),
end_row: adj_er0.map(|i| i + 1),
end_col: adj_ec0.map(|i| i + 1),
start_row_abs: *start_row_abs,
start_col_abs: *start_col_abs,
end_row_abs: *end_row_abs,
end_col_abs: *end_col_abs,
})
}
_ => ReferenceAdjustment::Reference(reference.clone()),
}
}
}
impl Default for ReferenceAdjuster {
fn default() -> Self {
Self::new()
}
}
pub struct RelativeReferenceAdjuster {
row_offset: i32,
col_offset: i32,
}
impl RelativeReferenceAdjuster {
pub fn new(row_offset: i32, col_offset: i32) -> Self {
Self {
row_offset,
col_offset,
}
}
pub fn adjust_formula(&self, ast: &ASTNode) -> ASTNode {
match &ast.node_type {
ASTNodeType::Reference {
original,
reference,
} => {
let adjusted = self.adjust_reference(reference);
ASTNode {
node_type: ASTNodeType::Reference {
original: original.clone(),
reference: adjusted,
},
source_token: ast.source_token.clone(),
contains_volatile: ast.contains_volatile,
}
}
ASTNodeType::BinaryOp { op, left, right } => ASTNode {
node_type: ASTNodeType::BinaryOp {
op: op.clone(),
left: Box::new(self.adjust_formula(left)),
right: Box::new(self.adjust_formula(right)),
},
source_token: ast.source_token.clone(),
contains_volatile: ast.contains_volatile,
},
ASTNodeType::UnaryOp { op, expr } => ASTNode {
node_type: ASTNodeType::UnaryOp {
op: op.clone(),
expr: Box::new(self.adjust_formula(expr)),
},
source_token: ast.source_token.clone(),
contains_volatile: ast.contains_volatile,
},
ASTNodeType::Function { name, args } => ASTNode {
node_type: ASTNodeType::Function {
name: name.clone(),
args: args.iter().map(|arg| self.adjust_formula(arg)).collect(),
},
source_token: ast.source_token.clone(),
contains_volatile: ast.contains_volatile,
},
_ => ast.clone(),
}
}
fn adjust_reference(
&self,
reference: &formualizer_parse::parser::ReferenceType,
) -> formualizer_parse::parser::ReferenceType {
use formualizer_parse::parser::ReferenceType;
let Some(shared) = reference.to_sheet_ref_lossy() else {
return reference.clone();
};
match (reference, shared) {
(ReferenceType::Cell { sheet, .. }, crate::reference::SharedRef::Cell(cell)) => {
let owned = cell.into_owned();
let row0 = owned.coord.row();
let col0 = owned.coord.col();
let row_abs = owned.coord.row_abs();
let col_abs = owned.coord.col_abs();
let new_row0 = if row_abs {
row0
} else {
(row0 as i32 + self.row_offset).max(0) as u32
};
let new_col0 = if col_abs {
col0
} else {
(col0 as i32 + self.col_offset).max(0) as u32
};
ReferenceType::Cell {
sheet: sheet.clone(),
row: new_row0 + 1,
col: new_col0 + 1,
row_abs,
col_abs,
}
}
(ReferenceType::Range { sheet, .. }, crate::reference::SharedRef::Range(range)) => {
let owned = range.into_owned();
let adj_axis = |b: formualizer_common::AxisBound, off: i32| {
if b.abs {
b.index
} else {
(b.index as i32 + off).max(0) as u32
}
};
let adj_start_row = owned.start_row.map(|b| adj_axis(b, self.row_offset) + 1);
let adj_start_col = owned.start_col.map(|b| adj_axis(b, self.col_offset) + 1);
let adj_end_row = owned.end_row.map(|b| adj_axis(b, self.row_offset) + 1);
let adj_end_col = owned.end_col.map(|b| adj_axis(b, self.col_offset) + 1);
let start_row_abs = owned.start_row.map(|b| b.abs).unwrap_or(false);
let start_col_abs = owned.start_col.map(|b| b.abs).unwrap_or(false);
let end_row_abs = owned.end_row.map(|b| b.abs).unwrap_or(false);
let end_col_abs = owned.end_col.map(|b| b.abs).unwrap_or(false);
ReferenceType::Range {
sheet: sheet.clone(),
start_row: adj_start_row,
start_col: adj_start_col,
end_row: adj_end_row,
end_col: adj_end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
}
}
_ => reference.clone(),
}
}
}
pub struct MoveReferenceAdjuster {
from_sheet_id: crate::SheetId,
from_sheet_name: String,
from_start_row: u32,
from_start_col: u32,
from_end_row: u32,
from_end_col: u32,
to_sheet_id: crate::SheetId,
to_sheet_name: String,
row_offset: i32,
col_offset: i32,
}
impl MoveReferenceAdjuster {
pub fn new(
from_sheet_id: crate::SheetId,
from_sheet_name: String,
from_start_row: u32,
from_start_col: u32,
from_end_row: u32,
from_end_col: u32,
to_sheet_id: crate::SheetId,
to_sheet_name: String,
row_offset: i32,
col_offset: i32,
) -> Self {
Self {
from_sheet_id,
from_sheet_name,
from_start_row,
from_start_col,
from_end_row,
from_end_col,
to_sheet_id,
to_sheet_name,
row_offset,
col_offset,
}
}
pub fn adjust_if_references(
&self,
formula: &ASTNode,
formula_sheet_id: crate::SheetId,
) -> Option<ASTNode> {
let (adjusted, changed) = self.adjust_ast_inner(formula, formula_sheet_id);
if changed { Some(adjusted) } else { None }
}
fn adjust_ast_inner(&self, ast: &ASTNode, formula_sheet_id: crate::SheetId) -> (ASTNode, bool) {
match &ast.node_type {
ASTNodeType::Reference {
original,
reference,
} => {
let (adjusted_ref, changed) = self.adjust_reference(reference, formula_sheet_id);
if !changed {
return (ast.clone(), false);
}
(
ASTNode {
node_type: ASTNodeType::Reference {
original: original.clone(),
reference: adjusted_ref,
},
source_token: ast.source_token.clone(),
contains_volatile: ast.contains_volatile,
},
true,
)
}
ASTNodeType::BinaryOp { op, left, right } => {
let (l_adj, l_ch) = self.adjust_ast_inner(left, formula_sheet_id);
let (r_adj, r_ch) = self.adjust_ast_inner(right, formula_sheet_id);
if !l_ch && !r_ch {
return (ast.clone(), false);
}
(
ASTNode {
node_type: ASTNodeType::BinaryOp {
op: op.clone(),
left: Box::new(l_adj),
right: Box::new(r_adj),
},
source_token: ast.source_token.clone(),
contains_volatile: ast.contains_volatile,
},
true,
)
}
ASTNodeType::UnaryOp { op, expr } => {
let (e_adj, e_ch) = self.adjust_ast_inner(expr, formula_sheet_id);
if !e_ch {
return (ast.clone(), false);
}
(
ASTNode {
node_type: ASTNodeType::UnaryOp {
op: op.clone(),
expr: Box::new(e_adj),
},
source_token: ast.source_token.clone(),
contains_volatile: ast.contains_volatile,
},
true,
)
}
ASTNodeType::Function { name, args } => {
let mut any = false;
let new_args: Vec<_> = args
.iter()
.map(|a| {
let (adj, ch) = self.adjust_ast_inner(a, formula_sheet_id);
any |= ch;
adj
})
.collect();
if !any {
return (ast.clone(), false);
}
(
ASTNode {
node_type: ASTNodeType::Function {
name: name.clone(),
args: new_args,
},
source_token: ast.source_token.clone(),
contains_volatile: ast.contains_volatile,
},
true,
)
}
ASTNodeType::Array(rows) => {
let mut any = false;
let new_rows: Vec<_> = rows
.iter()
.map(|row| {
row.iter()
.map(|c| {
let (adj, ch) = self.adjust_ast_inner(c, formula_sheet_id);
any |= ch;
adj
})
.collect()
})
.collect();
if !any {
return (ast.clone(), false);
}
(
ASTNode {
node_type: ASTNodeType::Array(new_rows),
source_token: ast.source_token.clone(),
contains_volatile: ast.contains_volatile,
},
true,
)
}
_ => (ast.clone(), false),
}
}
fn adjust_reference(
&self,
reference: &formualizer_parse::parser::ReferenceType,
formula_sheet_id: crate::SheetId,
) -> (formualizer_parse::parser::ReferenceType, bool) {
use formualizer_parse::parser::ReferenceType;
let sheet_matches_source = |sheet: &Option<String>| {
if let Some(name) = sheet.as_deref() {
name == self.from_sheet_name
} else {
formula_sheet_id == self.from_sheet_id
}
};
if !sheet_matches_source(match reference {
ReferenceType::Cell { sheet, .. } => sheet,
ReferenceType::Range { sheet, .. } => sheet,
_ => &None,
}) {
return (reference.clone(), false);
}
let Some(shared) = reference.to_sheet_ref_lossy() else {
return (reference.clone(), false);
};
match (reference, shared) {
(ReferenceType::Cell { sheet, .. }, crate::reference::SharedRef::Cell(cell)) => {
let owned = cell.into_owned();
let row0 = owned.coord.row();
let col0 = owned.coord.col();
let row_abs = owned.coord.row_abs();
let col_abs = owned.coord.col_abs();
if row0 < self.from_start_row
|| row0 > self.from_end_row
|| col0 < self.from_start_col
|| col0 > self.from_end_col
{
return (reference.clone(), false);
}
let new_row0 = (row0 as i32 + self.row_offset).max(0) as u32;
let new_col0 = (col0 as i32 + self.col_offset).max(0) as u32;
let new_sheet = if self.to_sheet_id != self.from_sheet_id {
Some(self.to_sheet_name.clone())
} else {
sheet.clone()
};
(
ReferenceType::Cell {
sheet: new_sheet,
row: new_row0 + 1,
col: new_col0 + 1,
row_abs,
col_abs,
},
true,
)
}
(ReferenceType::Range { sheet, .. }, crate::reference::SharedRef::Range(range)) => {
let owned = range.into_owned();
let (Some(sr), Some(sc), Some(er), Some(ec)) = (
owned.start_row,
owned.start_col,
owned.end_row,
owned.end_col,
) else {
return (reference.clone(), false);
};
let sr0 = sr.index;
let sc0 = sc.index;
let er0 = er.index;
let ec0 = ec.index;
let start_row_abs = sr.abs;
let start_col_abs = sc.abs;
let end_row_abs = er.abs;
let end_col_abs = ec.abs;
let fully_contained = sr0 >= self.from_start_row
&& er0 <= self.from_end_row
&& sc0 >= self.from_start_col
&& ec0 <= self.from_end_col;
if !fully_contained {
return (reference.clone(), false);
}
let new_sr0 = (sr0 as i32 + self.row_offset).max(0) as u32;
let new_er0 = (er0 as i32 + self.row_offset).max(0) as u32;
let new_sc0 = (sc0 as i32 + self.col_offset).max(0) as u32;
let new_ec0 = (ec0 as i32 + self.col_offset).max(0) as u32;
let new_sheet = if self.to_sheet_id != self.from_sheet_id {
Some(self.to_sheet_name.clone())
} else {
sheet.clone()
};
(
ReferenceType::Range {
sheet: new_sheet,
start_row: Some(new_sr0 + 1),
start_col: Some(new_sc0 + 1),
end_row: Some(new_er0 + 1),
end_col: Some(new_ec0 + 1),
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
},
true,
)
}
_ => (reference.clone(), false),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use formualizer_parse::parser::parse;
use std::sync::OnceLock;
fn context(sheet_id: SheetId) -> ReferenceContext<'static> {
static REGISTRY: OnceLock<SheetRegistry> = OnceLock::new();
let registry = REGISTRY.get_or_init(|| {
let mut registry = SheetRegistry::new();
registry.id_for("Sheet1");
registry.id_for("Other");
registry.id_for("#REF");
registry
});
ReferenceContext::new(sheet_id, registry)
}
fn format_formula(ast: &ASTNode) -> String {
format!("{ast:?}")
}
#[test]
fn context_free_adjuster_compatibility_api_remains_available() {
let adjuster = ReferenceAdjuster::new();
let ast = parse("=A1").unwrap();
let op = ShiftOperation::InsertRows {
sheet_id: 0,
before: 0,
count: 1,
};
let _ = adjuster.adjust_ast(&ast, &op);
let _ = adjuster.adjust_ast_with_policy(&ast, &op, AbsShiftPolicy::Track);
let _ = adjuster.adjust_ast_if_changed(&ast, &op);
let _ = adjuster.adjust_ast_if_changed_with_policy(&ast, &op, AbsShiftPolicy::Track);
}
#[test]
fn adjust_ast_if_changed_returns_none_for_unaffected_column_insert() {
let adjuster = ReferenceAdjuster::new();
let ast = parse("=A1+1").unwrap();
let adjusted = adjuster.adjust_ast_if_changed_in_context(
&ast,
&ShiftOperation::InsertColumns {
sheet_id: 0,
before: 3,
count: 1,
},
&context(0),
);
assert!(adjusted.is_none());
}
#[test]
fn adjust_ast_if_changed_returns_adjusted_for_insert_before_a() {
let adjuster = ReferenceAdjuster::new();
let ast = parse("=A1+1").unwrap();
let adjusted = adjuster
.adjust_ast_if_changed_in_context(
&ast,
&ShiftOperation::InsertColumns {
sheet_id: 0,
before: 0,
count: 1,
},
&context(0),
)
.expect("A1 reference should shift");
if let ASTNodeType::BinaryOp { left, .. } = &adjusted.node_type
&& let ASTNodeType::Reference {
reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
..
} = &left.node_type
{
assert_eq!(*row, 1);
assert_eq!(*col, 2);
return;
}
panic!("expected adjusted A1 reference to become B1");
}
#[test]
fn test_reference_adjustment_on_row_insert() {
let adjuster = ReferenceAdjuster::new();
let ast = parse("=A5+B10").unwrap();
let adjusted = adjuster.adjust_ast_in_context(
&ast,
&ShiftOperation::InsertRows {
sheet_id: 0,
before: 7,
count: 2,
},
&context(0),
);
if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
if let ASTNodeType::Reference {
reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
..
} = &left.node_type
{
assert_eq!(*row, 5); assert_eq!(*col, 1);
}
if let ASTNodeType::Reference {
reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
..
} = &right.node_type
{
assert_eq!(*row, 12); assert_eq!(*col, 2);
}
}
}
#[test]
fn test_reference_adjustment_on_column_delete() {
let adjuster = ReferenceAdjuster::new();
let ast = parse("=C1+F1").unwrap();
let adjusted = adjuster.adjust_ast_in_context(
&ast,
&ShiftOperation::DeleteColumns {
sheet_id: 0,
start: 2, count: 2,
},
&context(0),
);
let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type else {
panic!("expected adjusted binary expression, got {adjusted:?}");
};
match &left.node_type {
ASTNodeType::Literal(LiteralValue::Error(error)) => {
assert_eq!(error.kind, ExcelErrorKind::Ref);
assert!(left.source_token.is_none());
}
other => panic!("expected deleted C1 to become a #REF! literal, got {other:?}"),
}
match &right.node_type {
ASTNodeType::Reference {
original,
reference: ReferenceType::Cell { row, col, .. },
} => {
assert_eq!(original, "D1");
assert_eq!(*row, 1); assert_eq!(*col, 4); assert!(right.source_token.is_none());
}
other => panic!("expected surviving F1 reference to shift to D1, got {other:?}"),
}
assert!(adjusted.source_token.is_none());
}
#[test]
fn test_range_reference_adjustment() {
let adjuster = ReferenceAdjuster::new();
let ast = parse("=SUM(A1:A10)").unwrap();
let adjusted = adjuster.adjust_ast_in_context(
&ast,
&ShiftOperation::InsertRows {
sheet_id: 0,
before: 5,
count: 3,
},
&context(0),
);
if let ASTNodeType::Function { args, .. } = &adjusted.node_type
&& let Some(ASTNodeType::Reference {
reference:
formualizer_parse::parser::ReferenceType::Range {
start_row, end_row, ..
},
..
}) = args.first().map(|arg| &arg.node_type)
{
assert_eq!(start_row.unwrap_or(0), 1); assert_eq!(end_row.unwrap_or(0), 13); }
}
#[test]
fn test_relative_reference_copy() {
let adjuster = RelativeReferenceAdjuster::new(2, 3);
let ast = parse("=A1+B2").unwrap();
let adjusted = adjuster.adjust_formula(&ast);
if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
if let ASTNodeType::Reference {
reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
..
} = &left.node_type
{
assert_eq!(*row, 3); assert_eq!(*col, 4);
}
if let ASTNodeType::Reference {
reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
..
} = &right.node_type
{
assert_eq!(*row, 4); assert_eq!(*col, 5);
}
}
}
#[test]
fn test_absolute_row_tracks_row_insert() {
let adjuster = ReferenceAdjuster::new();
let cell_abs_row = CellRef::new(
0,
Coord::new(5, 2, true, false), );
let op = ShiftOperation::InsertRows {
sheet_id: 0,
before: 3,
count: 2,
};
let result = adjuster.adjust_cell_ref(&cell_abs_row, &op);
assert!(result.is_some());
let adjusted = result.unwrap();
assert_eq!(adjusted.coord.row(), 7); assert_eq!(adjusted.coord.col(), 2); assert!(adjusted.coord.row_abs()); assert!(!adjusted.coord.col_abs());
let pinned = adjuster
.adjust_cell_ref_with_policy(&cell_abs_row, &op, AbsShiftPolicy::Pin)
.unwrap();
assert_eq!(pinned.coord.row(), 5);
assert!(pinned.coord.row_abs());
}
#[test]
fn test_absolute_column_tracks_column_delete() {
let adjuster = ReferenceAdjuster::new();
let cell_abs_col = CellRef::new(
0,
Coord::new(5, 2, false, true), );
let op = ShiftOperation::DeleteColumns {
sheet_id: 0,
start: 1,
count: 1,
};
let result = adjuster.adjust_cell_ref(&cell_abs_col, &op);
assert!(result.is_some());
let adjusted = result.unwrap();
assert_eq!(adjusted.coord.row(), 5); assert_eq!(adjusted.coord.col(), 1); assert!(!adjusted.coord.row_abs());
assert!(adjusted.coord.col_abs());
let pinned = adjuster
.adjust_cell_ref_with_policy(&cell_abs_col, &op, AbsShiftPolicy::Pin)
.unwrap();
assert_eq!(pinned.coord.col(), 2);
assert!(pinned.coord.col_abs());
}
#[test]
fn test_absolute_reference_deleted_becomes_ref_error() {
let adjuster = ReferenceAdjuster::new();
let fully_abs = CellRef::new(0, Coord::new(0, 5, true, true));
let result = adjuster.adjust_cell_ref(
&fully_abs,
&ShiftOperation::DeleteRows {
sheet_id: 0,
start: 0,
count: 1,
},
);
assert!(result.is_none(), "deleted absolute target must be #REF!");
}
#[test]
fn test_mixed_absolute_relative_references() {
let adjuster = ReferenceAdjuster::new();
let mixed1 = CellRef::new(
0,
Coord::new(5, 1, false, true), );
let result1 = adjuster.adjust_cell_ref(
&mixed1,
&ShiftOperation::InsertRows {
sheet_id: 0,
before: 3,
count: 2,
},
);
assert!(result1.is_some());
let adj1 = result1.unwrap();
assert_eq!(adj1.coord.row(), 7); assert_eq!(adj1.coord.col(), 1);
let mixed2 = CellRef::new(
0,
Coord::new(10, 3, true, false), );
let result2 = adjuster.adjust_cell_ref(
&mixed2,
&ShiftOperation::DeleteColumns {
sheet_id: 0,
start: 1,
count: 1,
},
);
assert!(result2.is_some());
let adj2 = result2.unwrap();
assert_eq!(adj2.coord.row(), 10); assert_eq!(adj2.coord.col(), 2); }
#[test]
fn test_fully_absolute_reference_tracks_structural_ops() {
let adjuster = ReferenceAdjuster::new();
let fully_abs = CellRef::new(
0,
Coord::new(1, 1, true, true), );
let insert = ShiftOperation::InsertRows {
sheet_id: 0,
before: 1,
count: 5,
};
let result1 = adjuster.adjust_cell_ref(&fully_abs, &insert).unwrap();
assert_eq!(result1.coord.row(), 6); assert_eq!(result1.coord.col(), 1);
assert!(result1.coord.row_abs());
assert!(result1.coord.col_abs());
let delete = ShiftOperation::DeleteColumns {
sheet_id: 0,
start: 0,
count: 1,
};
let result2 = adjuster.adjust_cell_ref(&fully_abs, &delete).unwrap();
assert_eq!(result2.coord.row(), 1);
assert_eq!(result2.coord.col(), 0); assert!(result2.coord.row_abs());
assert!(result2.coord.col_abs());
let pinned1 = adjuster
.adjust_cell_ref_with_policy(&fully_abs, &insert, AbsShiftPolicy::Pin)
.unwrap();
assert_eq!(pinned1.coord.row(), 1);
assert_eq!(pinned1.coord.col(), 1);
let pinned2 = adjuster
.adjust_cell_ref_with_policy(&fully_abs, &delete, AbsShiftPolicy::Pin)
.unwrap();
assert_eq!(pinned2.coord.row(), 1);
assert_eq!(pinned2.coord.col(), 1);
}
#[test]
fn test_deleted_reference_becomes_ref_error() {
let adjuster = ReferenceAdjuster::new();
let cell = CellRef::new(
0,
Coord::new(5, 3, false, false), );
let result = adjuster.adjust_cell_ref(
&cell,
&ShiftOperation::DeleteRows {
sheet_id: 0,
start: 5,
count: 1,
},
);
assert!(result.is_none());
let result2 = adjuster.adjust_cell_ref(
&cell,
&ShiftOperation::DeleteColumns {
sheet_id: 0,
start: 3,
count: 1,
},
);
assert!(result2.is_none());
}
#[test]
fn test_range_expansion_on_insert() {
let adjuster = ReferenceAdjuster::new();
let ast = parse("=SUM(B2:D10)").unwrap();
let adjusted = adjuster.adjust_ast_in_context(
&ast,
&ShiftOperation::InsertRows {
sheet_id: 0,
before: 5,
count: 3,
},
&context(0),
);
if let ASTNodeType::Function { args, .. } = &adjusted.node_type
&& let Some(ASTNodeType::Reference {
reference:
formualizer_parse::parser::ReferenceType::Range {
start_row,
end_row,
start_col,
end_col,
..
},
..
}) = args.first().map(|arg| &arg.node_type)
{
assert_eq!(*start_row, Some(2)); assert_eq!(*end_row, Some(13)); assert_eq!(*start_col, Some(2)); assert_eq!(*end_col, Some(4)); }
}
#[test]
fn test_range_contraction_on_delete() {
let adjuster = ReferenceAdjuster::new();
let ast = parse("=SUM(A5:A20)").unwrap();
let adjusted = adjuster.adjust_ast_in_context(
&ast,
&ShiftOperation::DeleteRows {
sheet_id: 0,
start: 10,
count: 5,
},
&context(0),
);
if let ASTNodeType::Function { args, .. } = &adjusted.node_type
&& let Some(ASTNodeType::Reference {
reference:
formualizer_parse::parser::ReferenceType::Range {
start_row, end_row, ..
},
..
}) = args.first().map(|arg| &arg.node_type)
{
assert_eq!(*start_row, Some(5)); assert_eq!(*end_row, Some(15)); }
}
#[test]
fn fully_deleted_range_becomes_ref_error_literal() {
let ast = parse("=SUM(A2:A4)").unwrap();
let adjusted = ReferenceAdjuster::new().adjust_ast_in_context(
&ast,
&ShiftOperation::DeleteRows {
sheet_id: 0,
start: 1,
count: 3,
},
&context(0),
);
let ASTNodeType::Function { args, .. } = &adjusted.node_type else {
panic!("expected SUM function, got {adjusted:?}");
};
match &args[0].node_type {
ASTNodeType::Literal(LiteralValue::Error(error)) => {
assert_eq!(error.kind, ExcelErrorKind::Ref)
}
other => panic!("expected deleted range to become #REF!, got {other:?}"),
}
}
#[test]
fn structural_adjustment_respects_formula_and_qualified_sheets() {
let adjuster = ReferenceAdjuster::new();
let op = ShiftOperation::DeleteRows {
sheet_id: 0,
start: 0,
count: 1,
};
assert!(
adjuster
.adjust_ast_if_changed_in_context(&parse("=A1").unwrap(), &op, &context(1))
.is_none()
);
assert!(
adjuster
.adjust_ast_if_changed_in_context(&parse("=Other!A1").unwrap(), &op, &context(0))
.is_none()
);
let matching = adjuster
.adjust_ast_if_changed_in_context(&parse("=Sheet1!A1").unwrap(), &op, &context(1))
.expect("qualified reference to edited sheet must change");
assert!(matches!(
matching.node_type,
ASTNodeType::Literal(LiteralValue::Error(ref error))
if error.kind == ExcelErrorKind::Ref
));
let real_ref_sheet = parse("='#REF'!A1").unwrap();
assert!(
adjuster
.adjust_ast_if_changed_in_context(&real_ref_sheet, &op, &context(0))
.is_none()
);
let shifted = adjuster
.adjust_ast_if_changed_in_context(
&real_ref_sheet,
&ShiftOperation::InsertRows {
sheet_id: 2,
before: 0,
count: 1,
},
&context(0),
)
.expect("a real #REF sheet reference should shift normally");
match shifted.node_type {
ASTNodeType::Reference {
original,
reference:
ReferenceType::Cell {
sheet, row, col, ..
},
} => {
assert_eq!(original, "'#REF'!A2");
assert_eq!(sheet.as_deref(), Some("#REF"));
assert_eq!((row, col), (2, 1));
}
other => panic!("expected a shifted ordinary sheet reference, got {other:?}"),
}
}
#[test]
fn whole_axis_ranges_adjust_only_on_their_bounded_axis() {
let adjuster = ReferenceAdjuster::new();
let whole_col = parse("=SUM(A:A)").unwrap();
assert!(
adjuster
.adjust_ast_if_changed_in_context(
&whole_col,
&ShiftOperation::DeleteRows {
sheet_id: 0,
start: 0,
count: 1,
},
&context(0),
)
.is_none()
);
let deleted_col = adjuster.adjust_ast_in_context(
&whole_col,
&ShiftOperation::DeleteColumns {
sheet_id: 0,
start: 0,
count: 1,
},
&context(0),
);
let ASTNodeType::Function { args, .. } = &deleted_col.node_type else {
panic!("expected SUM function");
};
assert!(matches!(
args[0].node_type,
ASTNodeType::Literal(LiteralValue::Error(ref error))
if error.kind == ExcelErrorKind::Ref
));
let whole_row = parse("=SUM(1:1)").unwrap();
assert!(
adjuster
.adjust_ast_if_changed_in_context(
&whole_row,
&ShiftOperation::DeleteColumns {
sheet_id: 0,
start: 0,
count: 1,
},
&context(0),
)
.is_none()
);
let deleted_row = adjuster.adjust_ast_in_context(
&whole_row,
&ShiftOperation::DeleteRows {
sheet_id: 0,
start: 0,
count: 1,
},
&context(0),
);
let ASTNodeType::Function { args, .. } = &deleted_row.node_type else {
panic!("expected SUM function");
};
assert!(matches!(
args[0].node_type,
ASTNodeType::Literal(LiteralValue::Error(ref error))
if error.kind == ExcelErrorKind::Ref
));
}
#[test]
fn call_and_array_children_receive_literal_rewrites() {
let adjuster = ReferenceAdjuster::new();
let op = ShiftOperation::DeleteColumns {
sheet_id: 0,
start: 0,
count: 1,
};
let call =
adjuster.adjust_ast_in_context(&parse("=LAMBDA(x,x)(A1)").unwrap(), &op, &context(0));
let ASTNodeType::Call { args, .. } = &call.node_type else {
panic!("expected immediate call, got {call:?}");
};
assert!(matches!(
args[0].node_type,
ASTNodeType::Literal(LiteralValue::Error(ref error))
if error.kind == ExcelErrorKind::Ref
));
assert!(call.source_token.is_none());
let array = adjuster.adjust_ast_in_context(&parse("={A1,B1}").unwrap(), &op, &context(0));
let ASTNodeType::Array(rows) = &array.node_type else {
panic!("expected array, got {array:?}");
};
assert!(matches!(
rows[0][0].node_type,
ASTNodeType::Literal(LiteralValue::Error(ref error))
if error.kind == ExcelErrorKind::Ref
));
match &rows[0][1].node_type {
ASTNodeType::Reference {
original,
reference: ReferenceType::Cell { row, col, .. },
} => {
assert_eq!(original, "A1");
assert_eq!((*row, *col), (1, 1));
}
other => panic!("expected B1 to shift to A1, got {other:?}"),
}
}
}