formualizer_eval/engine/graph/editor/
reference_adjuster.rs

1use crate::reference::{CellRef, Coord};
2use formualizer_parse::parser::{ASTNode, ASTNodeType};
3
4/// Centralized reference adjustment logic for structural changes
5pub struct ReferenceAdjuster;
6
7#[derive(Debug, Clone)]
8pub enum ShiftOperation {
9    InsertRows {
10        sheet_id: u16,
11        before: u32,
12        count: u32,
13    },
14    DeleteRows {
15        sheet_id: u16,
16        start: u32,
17        count: u32,
18    },
19    InsertColumns {
20        sheet_id: u16,
21        before: u32,
22        count: u32,
23    },
24    DeleteColumns {
25        sheet_id: u16,
26        start: u32,
27        count: u32,
28    },
29}
30
31impl ReferenceAdjuster {
32    pub fn new() -> Self {
33        Self
34    }
35
36    /// Adjust an AST for a shift operation, preserving source tokens
37    pub fn adjust_ast(&self, ast: &ASTNode, op: &ShiftOperation) -> ASTNode {
38        match &ast.node_type {
39            ASTNodeType::Reference {
40                original,
41                reference,
42            } => {
43                let adjusted = self.adjust_reference(reference, op);
44                ASTNode {
45                    node_type: ASTNodeType::Reference {
46                        original: original.clone(),
47                        reference: adjusted,
48                    },
49                    source_token: ast.source_token.clone(),
50                    contains_volatile: ast.contains_volatile,
51                }
52            }
53            ASTNodeType::BinaryOp {
54                op: bin_op,
55                left,
56                right,
57            } => ASTNode {
58                node_type: ASTNodeType::BinaryOp {
59                    op: bin_op.clone(),
60                    left: Box::new(self.adjust_ast(left, op)),
61                    right: Box::new(self.adjust_ast(right, op)),
62                },
63                source_token: ast.source_token.clone(),
64                contains_volatile: ast.contains_volatile,
65            },
66            ASTNodeType::UnaryOp { op: un_op, expr } => ASTNode {
67                node_type: ASTNodeType::UnaryOp {
68                    op: un_op.clone(),
69                    expr: Box::new(self.adjust_ast(expr, op)),
70                },
71                source_token: ast.source_token.clone(),
72                contains_volatile: ast.contains_volatile,
73            },
74            ASTNodeType::Function { name, args } => ASTNode {
75                node_type: ASTNodeType::Function {
76                    name: name.clone(),
77                    args: args.iter().map(|arg| self.adjust_ast(arg, op)).collect(),
78                },
79                source_token: ast.source_token.clone(),
80                contains_volatile: ast.contains_volatile,
81            },
82            _ => ast.clone(),
83        }
84    }
85
86    /// Adjust a cell reference for a shift operation
87    /// Returns None if the cell is deleted
88    pub fn adjust_cell_ref(&self, cell_ref: &CellRef, op: &ShiftOperation) -> Option<CellRef> {
89        let coord = cell_ref.coord;
90        let adjusted_coord = match op {
91            ShiftOperation::InsertRows {
92                sheet_id,
93                before,
94                count,
95            } if cell_ref.sheet_id == *sheet_id => {
96                if coord.row_abs() || coord.row() < *before {
97                    // Absolute references or cells before insert point don't move
98                    coord
99                } else {
100                    // Shift down
101                    Coord::new(
102                        coord.row() + count,
103                        coord.col(),
104                        coord.row_abs(),
105                        coord.col_abs(),
106                    )
107                }
108            }
109            ShiftOperation::DeleteRows {
110                sheet_id,
111                start,
112                count,
113            } if cell_ref.sheet_id == *sheet_id => {
114                if coord.row_abs() {
115                    // Absolute references don't adjust
116                    coord
117                } else if coord.row() >= *start && coord.row() < start + count {
118                    // Cell deleted
119                    return None;
120                } else if coord.row() >= start + count {
121                    // Shift up
122                    Coord::new(
123                        coord.row() - count,
124                        coord.col(),
125                        coord.row_abs(),
126                        coord.col_abs(),
127                    )
128                } else {
129                    // Before delete range, no change
130                    coord
131                }
132            }
133            ShiftOperation::InsertColumns {
134                sheet_id,
135                before,
136                count,
137            } if cell_ref.sheet_id == *sheet_id => {
138                if coord.col_abs() || coord.col() < *before {
139                    // Absolute references or cells before insert point don't move
140                    coord
141                } else {
142                    // Shift right
143                    Coord::new(
144                        coord.row(),
145                        coord.col() + count,
146                        coord.row_abs(),
147                        coord.col_abs(),
148                    )
149                }
150            }
151            ShiftOperation::DeleteColumns {
152                sheet_id,
153                start,
154                count,
155            } if cell_ref.sheet_id == *sheet_id => {
156                if coord.col_abs() {
157                    // Absolute references don't adjust
158                    coord
159                } else if coord.col() >= *start && coord.col() < start + count {
160                    // Cell deleted
161                    return None;
162                } else if coord.col() >= start + count {
163                    // Shift left
164                    Coord::new(
165                        coord.row(),
166                        coord.col() - count,
167                        coord.row_abs(),
168                        coord.col_abs(),
169                    )
170                } else {
171                    // Before delete range, no change
172                    coord
173                }
174            }
175            _ => coord,
176        };
177
178        Some(CellRef::new(cell_ref.sheet_id, adjusted_coord))
179    }
180
181    /// Adjust a reference type (cell or range) for a shift operation
182    fn adjust_reference(
183        &self,
184        reference: &formualizer_parse::parser::ReferenceType,
185        op: &ShiftOperation,
186    ) -> formualizer_parse::parser::ReferenceType {
187        use formualizer_parse::parser::ReferenceType;
188
189        let shared = reference.to_sheet_ref_lossy();
190
191        match (reference, shared) {
192            (ReferenceType::Cell { sheet, .. }, Some(crate::reference::SharedRef::Cell(cell))) => {
193                let sheet_id = match op {
194                    ShiftOperation::InsertRows { sheet_id, .. }
195                    | ShiftOperation::DeleteRows { sheet_id, .. }
196                    | ShiftOperation::InsertColumns { sheet_id, .. }
197                    | ShiftOperation::DeleteColumns { sheet_id, .. } => *sheet_id,
198                };
199                let temp_ref = CellRef::new(
200                    sheet_id,
201                    Coord::new(cell.coord.row(), cell.coord.col(), false, false),
202                );
203
204                match self.adjust_cell_ref(&temp_ref, op) {
205                    None => ReferenceType::Cell {
206                        sheet: Some("#REF".to_string()),
207                        row: 0,
208                        col: 0,
209                    },
210                    Some(adjusted) => ReferenceType::Cell {
211                        sheet: sheet.clone(),
212                        row: adjusted.coord.row() + 1,
213                        col: adjusted.coord.col() + 1,
214                    },
215                }
216            }
217            (
218                ReferenceType::Range { sheet, .. },
219                Some(crate::reference::SharedRef::Range(range)),
220            ) => {
221                let is_unbounded_column = range.start_row.is_none() && range.end_row.is_none();
222                let is_unbounded_row = range.start_col.is_none() && range.end_col.is_none();
223                if is_unbounded_column || is_unbounded_row {
224                    return reference.clone();
225                }
226
227                let sr0 = range.start_row.map(|b| b.index);
228                let sc0 = range.start_col.map(|b| b.index);
229                let er0 = range.end_row.map(|b| b.index);
230                let ec0 = range.end_col.map(|b| b.index);
231
232                let (adj_sr0, adj_er0) = match op {
233                    ShiftOperation::InsertRows { before, count, .. } => match (sr0, er0) {
234                        (Some(start), Some(end)) => {
235                            let adj_start = if start >= *before {
236                                start + count
237                            } else {
238                                start
239                            };
240                            let adj_end = if end >= *before { end + count } else { end };
241                            (Some(adj_start), Some(adj_end))
242                        }
243                        _ => (sr0, er0),
244                    },
245                    ShiftOperation::DeleteRows { start, count, .. } => match (sr0, er0) {
246                        (Some(range_start), Some(range_end)) => {
247                            if range_end < *start || range_start >= start + count {
248                                let adj_start = if range_start >= start + count {
249                                    range_start - count
250                                } else {
251                                    range_start
252                                };
253                                let adj_end = if range_end >= start + count {
254                                    range_end - count
255                                } else {
256                                    range_end
257                                };
258                                (Some(adj_start), Some(adj_end))
259                            } else if range_start >= *start && range_end < start + count {
260                                return ReferenceType::Range {
261                                    sheet: Some("#REF".to_string()),
262                                    start_row: Some(0),
263                                    start_col: Some(0),
264                                    end_row: Some(0),
265                                    end_col: Some(0),
266                                };
267                            } else {
268                                let adj_start = if range_start < *start {
269                                    range_start
270                                } else {
271                                    *start
272                                };
273                                let adj_end = if range_end >= start + count {
274                                    range_end - count
275                                } else {
276                                    start.saturating_sub(1)
277                                };
278                                (Some(adj_start), Some(adj_end))
279                            }
280                        }
281                        _ => (sr0, er0),
282                    },
283                    _ => (sr0, er0),
284                };
285
286                let (adj_sc0, adj_ec0) = match op {
287                    ShiftOperation::InsertColumns { before, count, .. } => match (sc0, ec0) {
288                        (Some(start), Some(end)) => {
289                            let adj_start = if start >= *before {
290                                start + count
291                            } else {
292                                start
293                            };
294                            let adj_end = if end >= *before { end + count } else { end };
295                            (Some(adj_start), Some(adj_end))
296                        }
297                        _ => (sc0, ec0),
298                    },
299                    ShiftOperation::DeleteColumns { start, count, .. } => match (sc0, ec0) {
300                        (Some(range_start), Some(range_end)) => {
301                            if range_end < *start || range_start >= start + count {
302                                let adj_start = if range_start >= start + count {
303                                    range_start - count
304                                } else {
305                                    range_start
306                                };
307                                let adj_end = if range_end >= start + count {
308                                    range_end - count
309                                } else {
310                                    range_end
311                                };
312                                (Some(adj_start), Some(adj_end))
313                            } else if range_start >= *start && range_end < start + count {
314                                return ReferenceType::Range {
315                                    sheet: Some("#REF".to_string()),
316                                    start_row: Some(0),
317                                    start_col: Some(0),
318                                    end_row: Some(0),
319                                    end_col: Some(0),
320                                };
321                            } else {
322                                let adj_start = if range_start < *start {
323                                    range_start
324                                } else {
325                                    *start
326                                };
327                                let adj_end = if range_end >= start + count {
328                                    range_end - count
329                                } else {
330                                    start.saturating_sub(1)
331                                };
332                                (Some(adj_start), Some(adj_end))
333                            }
334                        }
335                        _ => (sc0, ec0),
336                    },
337                    _ => (sc0, ec0),
338                };
339
340                ReferenceType::Range {
341                    sheet: sheet.clone(),
342                    start_row: adj_sr0.map(|i| i + 1),
343                    start_col: adj_sc0.map(|i| i + 1),
344                    end_row: adj_er0.map(|i| i + 1),
345                    end_col: adj_ec0.map(|i| i + 1),
346                }
347            }
348            _ => reference.clone(),
349        }
350    }
351}
352
353impl Default for ReferenceAdjuster {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359/// Helper for adjusting references when copying/moving ranges
360pub struct RelativeReferenceAdjuster {
361    row_offset: i32,
362    col_offset: i32,
363}
364
365impl RelativeReferenceAdjuster {
366    pub fn new(row_offset: i32, col_offset: i32) -> Self {
367        Self {
368            row_offset,
369            col_offset,
370        }
371    }
372
373    pub fn adjust_formula(&self, ast: &ASTNode) -> ASTNode {
374        match &ast.node_type {
375            ASTNodeType::Reference {
376                original,
377                reference,
378            } => {
379                let adjusted = self.adjust_reference(reference);
380                ASTNode {
381                    node_type: ASTNodeType::Reference {
382                        original: original.clone(),
383                        reference: adjusted,
384                    },
385                    source_token: ast.source_token.clone(),
386                    contains_volatile: ast.contains_volatile,
387                }
388            }
389            ASTNodeType::BinaryOp { op, left, right } => ASTNode {
390                node_type: ASTNodeType::BinaryOp {
391                    op: op.clone(),
392                    left: Box::new(self.adjust_formula(left)),
393                    right: Box::new(self.adjust_formula(right)),
394                },
395                source_token: ast.source_token.clone(),
396                contains_volatile: ast.contains_volatile,
397            },
398            ASTNodeType::UnaryOp { op, expr } => ASTNode {
399                node_type: ASTNodeType::UnaryOp {
400                    op: op.clone(),
401                    expr: Box::new(self.adjust_formula(expr)),
402                },
403                source_token: ast.source_token.clone(),
404                contains_volatile: ast.contains_volatile,
405            },
406            ASTNodeType::Function { name, args } => ASTNode {
407                node_type: ASTNodeType::Function {
408                    name: name.clone(),
409                    args: args.iter().map(|arg| self.adjust_formula(arg)).collect(),
410                },
411                source_token: ast.source_token.clone(),
412                contains_volatile: ast.contains_volatile,
413            },
414            _ => ast.clone(),
415        }
416    }
417
418    fn adjust_reference(
419        &self,
420        reference: &formualizer_parse::parser::ReferenceType,
421    ) -> formualizer_parse::parser::ReferenceType {
422        use formualizer_parse::parser::ReferenceType;
423
424        let Some(shared) = reference.to_sheet_ref_lossy() else {
425            return reference.clone();
426        };
427
428        match (reference, shared) {
429            (ReferenceType::Cell { sheet, .. }, crate::reference::SharedRef::Cell(cell)) => {
430                let owned = cell.into_owned();
431                let row0 = owned.coord.row();
432                let col0 = owned.coord.col();
433                let row_abs = owned.coord.row_abs();
434                let col_abs = owned.coord.col_abs();
435
436                let new_row0 = if row_abs {
437                    row0
438                } else {
439                    (row0 as i32 + self.row_offset).max(0) as u32
440                };
441                let new_col0 = if col_abs {
442                    col0
443                } else {
444                    (col0 as i32 + self.col_offset).max(0) as u32
445                };
446
447                ReferenceType::Cell {
448                    sheet: sheet.clone(),
449                    row: new_row0 + 1,
450                    col: new_col0 + 1,
451                }
452            }
453            (ReferenceType::Range { sheet, .. }, crate::reference::SharedRef::Range(range)) => {
454                let owned = range.into_owned();
455
456                let adj_axis = |b: formualizer_common::AxisBound, off: i32| {
457                    if b.abs {
458                        b.index
459                    } else {
460                        (b.index as i32 + off).max(0) as u32
461                    }
462                };
463
464                let adj_start_row = owned.start_row.map(|b| adj_axis(b, self.row_offset) + 1);
465                let adj_start_col = owned.start_col.map(|b| adj_axis(b, self.col_offset) + 1);
466                let adj_end_row = owned.end_row.map(|b| adj_axis(b, self.row_offset) + 1);
467                let adj_end_col = owned.end_col.map(|b| adj_axis(b, self.col_offset) + 1);
468
469                ReferenceType::Range {
470                    sheet: sheet.clone(),
471                    start_row: adj_start_row,
472                    start_col: adj_start_col,
473                    end_row: adj_end_row,
474                    end_col: adj_end_col,
475                }
476            }
477            _ => reference.clone(),
478        }
479    }
480}
481
482/// Helper for adjusting references to moved ranges.
483/// This is used when a block of cells is moved; any formula references to cells
484/// fully inside the source rectangle are translated to the destination.
485pub struct MoveReferenceAdjuster {
486    from_sheet_id: crate::SheetId,
487    from_sheet_name: String,
488    from_start_row: u32,
489    from_start_col: u32,
490    from_end_row: u32,
491    from_end_col: u32,
492    to_sheet_id: crate::SheetId,
493    to_sheet_name: String,
494    row_offset: i32,
495    col_offset: i32,
496}
497
498impl MoveReferenceAdjuster {
499    pub fn new(
500        from_sheet_id: crate::SheetId,
501        from_sheet_name: String,
502        from_start_row: u32,
503        from_start_col: u32,
504        from_end_row: u32,
505        from_end_col: u32,
506        to_sheet_id: crate::SheetId,
507        to_sheet_name: String,
508        row_offset: i32,
509        col_offset: i32,
510    ) -> Self {
511        Self {
512            from_sheet_id,
513            from_sheet_name,
514            from_start_row,
515            from_start_col,
516            from_end_row,
517            from_end_col,
518            to_sheet_id,
519            to_sheet_name,
520            row_offset,
521            col_offset,
522        }
523    }
524
525    pub fn adjust_if_references(
526        &self,
527        formula: &ASTNode,
528        formula_sheet_id: crate::SheetId,
529    ) -> Option<ASTNode> {
530        let (adjusted, changed) = self.adjust_ast_inner(formula, formula_sheet_id);
531        if changed { Some(adjusted) } else { None }
532    }
533
534    fn adjust_ast_inner(&self, ast: &ASTNode, formula_sheet_id: crate::SheetId) -> (ASTNode, bool) {
535        match &ast.node_type {
536            ASTNodeType::Reference {
537                original,
538                reference,
539            } => {
540                let (adjusted_ref, changed) = self.adjust_reference(reference, formula_sheet_id);
541                if !changed {
542                    return (ast.clone(), false);
543                }
544                (
545                    ASTNode {
546                        node_type: ASTNodeType::Reference {
547                            original: original.clone(),
548                            reference: adjusted_ref,
549                        },
550                        source_token: ast.source_token.clone(),
551                        contains_volatile: ast.contains_volatile,
552                    },
553                    true,
554                )
555            }
556            ASTNodeType::BinaryOp { op, left, right } => {
557                let (l_adj, l_ch) = self.adjust_ast_inner(left, formula_sheet_id);
558                let (r_adj, r_ch) = self.adjust_ast_inner(right, formula_sheet_id);
559                if !l_ch && !r_ch {
560                    return (ast.clone(), false);
561                }
562                (
563                    ASTNode {
564                        node_type: ASTNodeType::BinaryOp {
565                            op: op.clone(),
566                            left: Box::new(l_adj),
567                            right: Box::new(r_adj),
568                        },
569                        source_token: ast.source_token.clone(),
570                        contains_volatile: ast.contains_volatile,
571                    },
572                    true,
573                )
574            }
575            ASTNodeType::UnaryOp { op, expr } => {
576                let (e_adj, e_ch) = self.adjust_ast_inner(expr, formula_sheet_id);
577                if !e_ch {
578                    return (ast.clone(), false);
579                }
580                (
581                    ASTNode {
582                        node_type: ASTNodeType::UnaryOp {
583                            op: op.clone(),
584                            expr: Box::new(e_adj),
585                        },
586                        source_token: ast.source_token.clone(),
587                        contains_volatile: ast.contains_volatile,
588                    },
589                    true,
590                )
591            }
592            ASTNodeType::Function { name, args } => {
593                let mut any = false;
594                let new_args: Vec<_> = args
595                    .iter()
596                    .map(|a| {
597                        let (adj, ch) = self.adjust_ast_inner(a, formula_sheet_id);
598                        any |= ch;
599                        adj
600                    })
601                    .collect();
602                if !any {
603                    return (ast.clone(), false);
604                }
605                (
606                    ASTNode {
607                        node_type: ASTNodeType::Function {
608                            name: name.clone(),
609                            args: new_args,
610                        },
611                        source_token: ast.source_token.clone(),
612                        contains_volatile: ast.contains_volatile,
613                    },
614                    true,
615                )
616            }
617            ASTNodeType::Array(rows) => {
618                let mut any = false;
619                let new_rows: Vec<_> = rows
620                    .iter()
621                    .map(|row| {
622                        row.iter()
623                            .map(|c| {
624                                let (adj, ch) = self.adjust_ast_inner(c, formula_sheet_id);
625                                any |= ch;
626                                adj
627                            })
628                            .collect()
629                    })
630                    .collect();
631                if !any {
632                    return (ast.clone(), false);
633                }
634                (
635                    ASTNode {
636                        node_type: ASTNodeType::Array(new_rows),
637                        source_token: ast.source_token.clone(),
638                        contains_volatile: ast.contains_volatile,
639                    },
640                    true,
641                )
642            }
643            _ => (ast.clone(), false),
644        }
645    }
646
647    fn adjust_reference(
648        &self,
649        reference: &formualizer_parse::parser::ReferenceType,
650        formula_sheet_id: crate::SheetId,
651    ) -> (formualizer_parse::parser::ReferenceType, bool) {
652        use formualizer_parse::parser::ReferenceType;
653
654        let sheet_matches_source = |sheet: &Option<String>| {
655            if let Some(name) = sheet.as_deref() {
656                name == self.from_sheet_name
657            } else {
658                formula_sheet_id == self.from_sheet_id
659            }
660        };
661
662        if !sheet_matches_source(match reference {
663            ReferenceType::Cell { sheet, .. } => sheet,
664            ReferenceType::Range { sheet, .. } => sheet,
665            _ => &None,
666        }) {
667            return (reference.clone(), false);
668        }
669
670        let Some(shared) = reference.to_sheet_ref_lossy() else {
671            return (reference.clone(), false);
672        };
673
674        match (reference, shared) {
675            (ReferenceType::Cell { sheet, .. }, crate::reference::SharedRef::Cell(cell)) => {
676                let owned = cell.into_owned();
677                let row0 = owned.coord.row();
678                let col0 = owned.coord.col();
679
680                if row0 < self.from_start_row
681                    || row0 > self.from_end_row
682                    || col0 < self.from_start_col
683                    || col0 > self.from_end_col
684                {
685                    return (reference.clone(), false);
686                }
687
688                let new_row0 = (row0 as i32 + self.row_offset).max(0) as u32;
689                let new_col0 = (col0 as i32 + self.col_offset).max(0) as u32;
690
691                let new_sheet = if self.to_sheet_id != self.from_sheet_id {
692                    Some(self.to_sheet_name.clone())
693                } else {
694                    sheet.clone()
695                };
696
697                (
698                    ReferenceType::Cell {
699                        sheet: new_sheet,
700                        row: new_row0 + 1,
701                        col: new_col0 + 1,
702                    },
703                    true,
704                )
705            }
706            (ReferenceType::Range { sheet, .. }, crate::reference::SharedRef::Range(range)) => {
707                let owned = range.into_owned();
708                let (Some(sr), Some(sc), Some(er), Some(ec)) = (
709                    owned.start_row,
710                    owned.start_col,
711                    owned.end_row,
712                    owned.end_col,
713                ) else {
714                    return (reference.clone(), false);
715                };
716
717                let sr0 = sr.index;
718                let sc0 = sc.index;
719                let er0 = er.index;
720                let ec0 = ec.index;
721
722                let fully_contained = sr0 >= self.from_start_row
723                    && er0 <= self.from_end_row
724                    && sc0 >= self.from_start_col
725                    && ec0 <= self.from_end_col;
726                if !fully_contained {
727                    return (reference.clone(), false);
728                }
729
730                let new_sr0 = (sr0 as i32 + self.row_offset).max(0) as u32;
731                let new_er0 = (er0 as i32 + self.row_offset).max(0) as u32;
732                let new_sc0 = (sc0 as i32 + self.col_offset).max(0) as u32;
733                let new_ec0 = (ec0 as i32 + self.col_offset).max(0) as u32;
734
735                let new_sheet = if self.to_sheet_id != self.from_sheet_id {
736                    Some(self.to_sheet_name.clone())
737                } else {
738                    sheet.clone()
739                };
740
741                (
742                    ReferenceType::Range {
743                        sheet: new_sheet,
744                        start_row: Some(new_sr0 + 1),
745                        start_col: Some(new_sc0 + 1),
746                        end_row: Some(new_er0 + 1),
747                        end_col: Some(new_ec0 + 1),
748                    },
749                    true,
750                )
751            }
752            _ => (reference.clone(), false),
753        }
754    }
755}
756
757#[cfg(test)]
758mod tests {
759    use super::*;
760    use formualizer_parse::parser::parse;
761
762    fn format_formula(ast: &ASTNode) -> String {
763        // TODO: Use the actual formualizer_parse::parser::to_string when available
764        // For now, a simple representation
765        format!("{ast:?}")
766    }
767
768    #[test]
769    fn test_reference_adjustment_on_row_insert() {
770        let adjuster = ReferenceAdjuster::new();
771
772        // Formula: =A5+B10
773        let ast = parse("=A5+B10").unwrap();
774
775        // Insert 2 rows before row 7
776        let adjusted = adjuster.adjust_ast(
777            &ast,
778            &ShiftOperation::InsertRows {
779                sheet_id: 0,
780                before: 7,
781                count: 2,
782            },
783        );
784
785        // A5 unchanged (before insert point), B10 -> B12
786        // Verify by checking the AST structure
787        if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
788            if let ASTNodeType::Reference {
789                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
790                ..
791            } = &left.node_type
792            {
793                assert_eq!(*row, 5); // A5 unchanged
794                assert_eq!(*col, 1);
795            }
796            if let ASTNodeType::Reference {
797                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
798                ..
799            } = &right.node_type
800            {
801                assert_eq!(*row, 12); // B10 -> B12
802                assert_eq!(*col, 2);
803            }
804        }
805    }
806
807    #[test]
808    fn test_reference_adjustment_on_column_delete() {
809        let adjuster = ReferenceAdjuster::new();
810
811        // Formula: =C1+F1
812        let ast = parse("=C1+F1").unwrap();
813
814        // Delete columns B and C (columns 2 and 3)
815        let adjusted = adjuster.adjust_ast(
816            &ast,
817            &ShiftOperation::DeleteColumns {
818                sheet_id: 0,
819                start: 2, // Column B
820                count: 2,
821            },
822        );
823
824        // C1 -> #REF! (deleted), F1 -> D1 (shifted left by 2)
825        if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
826            if let ASTNodeType::Reference {
827                reference: formualizer_parse::parser::ReferenceType::Cell { sheet, row, col },
828                ..
829            } = &left.node_type
830            {
831                assert_eq!(sheet.as_deref(), Some("#REF"));
832                assert_eq!(*row, 0);
833                assert_eq!(*col, 0);
834            }
835            if let ASTNodeType::Reference {
836                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
837                ..
838            } = &right.node_type
839            {
840                assert_eq!(*row, 1); // Row unchanged
841                assert_eq!(*col, 4); // F1 (col 6) -> D1 (col 4)
842            }
843        }
844    }
845
846    #[test]
847    fn test_range_reference_adjustment() {
848        let adjuster = ReferenceAdjuster::new();
849
850        // Formula: =SUM(A1:A10)
851        let ast = parse("=SUM(A1:A10)").unwrap();
852
853        // Insert 3 rows before row 5
854        let adjusted = adjuster.adjust_ast(
855            &ast,
856            &ShiftOperation::InsertRows {
857                sheet_id: 0,
858                before: 5,
859                count: 3,
860            },
861        );
862
863        // Range should expand: A1:A10 -> A1:A13
864        if let ASTNodeType::Function { args, .. } = &adjusted.node_type
865            && let Some(ASTNodeType::Reference {
866                reference:
867                    formualizer_parse::parser::ReferenceType::Range {
868                        start_row, end_row, ..
869                    },
870                ..
871            }) = args.first().map(|arg| &arg.node_type)
872        {
873            assert_eq!(start_row.unwrap_or(0), 1); // A1 start unchanged
874            assert_eq!(end_row.unwrap_or(0), 13); // A10 -> A13
875        }
876    }
877
878    #[test]
879    fn test_relative_reference_copy() {
880        let adjuster = RelativeReferenceAdjuster::new(2, 3); // Move 2 rows down, 3 cols right
881
882        // Formula: =A1+B2
883        let ast = parse("=A1+B2").unwrap();
884        let adjusted = adjuster.adjust_formula(&ast);
885
886        // A1 -> D3, B2 -> E4
887        if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
888            if let ASTNodeType::Reference {
889                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
890                ..
891            } = &left.node_type
892            {
893                assert_eq!(*row, 3); // A1 (1,1) -> D3 (3,4)
894                assert_eq!(*col, 4);
895            }
896            if let ASTNodeType::Reference {
897                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
898                ..
899            } = &right.node_type
900            {
901                assert_eq!(*row, 4); // B2 (2,2) -> E4 (4,5)
902                assert_eq!(*col, 5);
903            }
904        }
905    }
906
907    #[test]
908    fn test_absolute_reference_preservation() {
909        let adjuster = ReferenceAdjuster::new();
910
911        // Test with absolute row references ($5)
912        let cell_abs_row = CellRef::new(
913            0,
914            Coord::new(5, 2, true, false), // Row 5 absolute, col 2 relative
915        );
916
917        // Insert rows before the absolute reference
918        let result = adjuster.adjust_cell_ref(
919            &cell_abs_row,
920            &ShiftOperation::InsertRows {
921                sheet_id: 0,
922                before: 3,
923                count: 2,
924            },
925        );
926
927        // Absolute row should not change
928        assert!(result.is_some());
929        let adjusted = result.unwrap();
930        assert_eq!(adjusted.coord.row(), 5); // Row stays at 5
931        assert_eq!(adjusted.coord.col(), 2); // Column unchanged
932        assert!(adjusted.coord.row_abs());
933        assert!(!adjusted.coord.col_abs());
934    }
935
936    #[test]
937    fn test_absolute_column_preservation() {
938        let adjuster = ReferenceAdjuster::new();
939
940        // Test with absolute column references ($B)
941        let cell_abs_col = CellRef::new(
942            0,
943            Coord::new(5, 2, false, true), // Row 5 relative, col 2 absolute
944        );
945
946        // Delete columns before the absolute reference
947        let result = adjuster.adjust_cell_ref(
948            &cell_abs_col,
949            &ShiftOperation::DeleteColumns {
950                sheet_id: 0,
951                start: 1,
952                count: 1,
953            },
954        );
955
956        // Absolute column should not change
957        assert!(result.is_some());
958        let adjusted = result.unwrap();
959        assert_eq!(adjusted.coord.row(), 5); // Row unchanged
960        assert_eq!(adjusted.coord.col(), 2); // Column stays at 2 despite deletion
961        assert!(!adjusted.coord.row_abs());
962        assert!(adjusted.coord.col_abs());
963    }
964
965    #[test]
966    fn test_mixed_absolute_relative_references() {
967        let adjuster = ReferenceAdjuster::new();
968
969        // Test 1: $A5 (col absolute, row relative) with row insertion
970        let mixed1 = CellRef::new(
971            0,
972            Coord::new(5, 1, false, true), // Row 5 relative, col 1 absolute
973        );
974
975        let result1 = adjuster.adjust_cell_ref(
976            &mixed1,
977            &ShiftOperation::InsertRows {
978                sheet_id: 0,
979                before: 3,
980                count: 2,
981            },
982        );
983
984        assert!(result1.is_some());
985        let adj1 = result1.unwrap();
986        assert_eq!(adj1.coord.row(), 7); // Row 5 -> 7 (shifted)
987        assert_eq!(adj1.coord.col(), 1); // Column stays at 1 (absolute)
988
989        // Test 2: B$10 (col relative, row absolute) with column deletion
990        let mixed2 = CellRef::new(
991            0,
992            Coord::new(10, 3, true, false), // Row 10 absolute, col 3 relative
993        );
994
995        let result2 = adjuster.adjust_cell_ref(
996            &mixed2,
997            &ShiftOperation::DeleteColumns {
998                sheet_id: 0,
999                start: 1,
1000                count: 1,
1001            },
1002        );
1003
1004        assert!(result2.is_some());
1005        let adj2 = result2.unwrap();
1006        assert_eq!(adj2.coord.row(), 10); // Row stays at 10 (absolute)
1007        assert_eq!(adj2.coord.col(), 2); // Column 3 -> 2 (shifted left)
1008    }
1009
1010    #[test]
1011    fn test_fully_absolute_reference() {
1012        let adjuster = ReferenceAdjuster::new();
1013
1014        // Test $A$1 - fully absolute
1015        let fully_abs = CellRef::new(
1016            0,
1017            Coord::new(1, 1, true, true), // Both row and col absolute
1018        );
1019
1020        // Try various operations - nothing should change
1021
1022        // Insert rows
1023        let result1 = adjuster.adjust_cell_ref(
1024            &fully_abs,
1025            &ShiftOperation::InsertRows {
1026                sheet_id: 0,
1027                before: 1,
1028                count: 5,
1029            },
1030        );
1031        assert!(result1.is_some());
1032        assert_eq!(result1.unwrap().coord.row(), 1);
1033        assert_eq!(result1.unwrap().coord.col(), 1);
1034
1035        // Delete columns
1036        let result2 = adjuster.adjust_cell_ref(
1037            &fully_abs,
1038            &ShiftOperation::DeleteColumns {
1039                sheet_id: 0,
1040                start: 0,
1041                count: 1,
1042            },
1043        );
1044        assert!(result2.is_some());
1045        assert_eq!(result2.unwrap().coord.row(), 1);
1046        assert_eq!(result2.unwrap().coord.col(), 1);
1047    }
1048
1049    #[test]
1050    fn test_deleted_reference_becomes_ref_error() {
1051        let adjuster = ReferenceAdjuster::new();
1052
1053        // Test deleting a cell that's referenced
1054        let cell = CellRef::new(
1055            0,
1056            Coord::new(5, 3, false, false), // Row 5, col 3, both relative
1057        );
1058
1059        // Delete the row containing the cell
1060        let result = adjuster.adjust_cell_ref(
1061            &cell,
1062            &ShiftOperation::DeleteRows {
1063                sheet_id: 0,
1064                start: 5,
1065                count: 1,
1066            },
1067        );
1068
1069        // Should return None to indicate deletion
1070        assert!(result.is_none());
1071
1072        // Delete the column containing the cell
1073        let result2 = adjuster.adjust_cell_ref(
1074            &cell,
1075            &ShiftOperation::DeleteColumns {
1076                sheet_id: 0,
1077                start: 3,
1078                count: 1,
1079            },
1080        );
1081
1082        // Should return None to indicate deletion
1083        assert!(result2.is_none());
1084    }
1085
1086    #[test]
1087    fn test_range_expansion_on_insert() {
1088        let adjuster = ReferenceAdjuster::new();
1089
1090        // Test that ranges expand when rows/cols are inserted within them
1091        let ast = parse("=SUM(B2:D10)").unwrap();
1092
1093        // Insert rows in the middle of the range
1094        let adjusted = adjuster.adjust_ast(
1095            &ast,
1096            &ShiftOperation::InsertRows {
1097                sheet_id: 0,
1098                before: 5,
1099                count: 3,
1100            },
1101        );
1102
1103        // Range should expand: B2:D10 -> B2:D13
1104        if let ASTNodeType::Function { args, .. } = &adjusted.node_type
1105            && let Some(ASTNodeType::Reference {
1106                reference:
1107                    formualizer_parse::parser::ReferenceType::Range {
1108                        start_row,
1109                        end_row,
1110                        start_col,
1111                        end_col,
1112                        ..
1113                    },
1114                ..
1115            }) = args.first().map(|arg| &arg.node_type)
1116        {
1117            assert_eq!(*start_row, Some(2)); // Start unchanged
1118            assert_eq!(*end_row, Some(13)); // End expanded from 10 to 13
1119            assert_eq!(*start_col, Some(2)); // B column
1120            assert_eq!(*end_col, Some(4)); // D column
1121        }
1122    }
1123
1124    #[test]
1125    fn test_range_contraction_on_delete() {
1126        let adjuster = ReferenceAdjuster::new();
1127
1128        // Test that ranges contract when rows/cols are deleted within them
1129        let ast = parse("=SUM(A5:A20)").unwrap();
1130
1131        // Delete rows in the middle of the range
1132        let adjusted = adjuster.adjust_ast(
1133            &ast,
1134            &ShiftOperation::DeleteRows {
1135                sheet_id: 0,
1136                start: 10,
1137                count: 5,
1138            },
1139        );
1140
1141        // Range should contract: A5:A20 -> A5:A15
1142        if let ASTNodeType::Function { args, .. } = &adjusted.node_type
1143            && let Some(ASTNodeType::Reference {
1144                reference:
1145                    formualizer_parse::parser::ReferenceType::Range {
1146                        start_row, end_row, ..
1147                    },
1148                ..
1149            }) = args.first().map(|arg| &arg.node_type)
1150        {
1151            assert_eq!(*start_row, Some(5)); // Start unchanged
1152            assert_eq!(*end_row, Some(15)); // End contracted from 20 to 15
1153        }
1154    }
1155}