1use crate::reference::{CellRef, Coord};
2use formualizer_parse::parser::{ASTNode, ASTNodeType};
3
4pub 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 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 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 coord
99 } else {
100 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 coord
117 } else if coord.row() >= *start && coord.row() < start + count {
118 return None;
120 } else if coord.row() >= start + count {
121 Coord::new(
123 coord.row() - count,
124 coord.col(),
125 coord.row_abs(),
126 coord.col_abs(),
127 )
128 } else {
129 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 coord
141 } else {
142 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 coord
159 } else if coord.col() >= *start && coord.col() < start + count {
160 return None;
162 } else if coord.col() >= start + count {
163 Coord::new(
165 coord.row(),
166 coord.col() - count,
167 coord.row_abs(),
168 coord.col_abs(),
169 )
170 } else {
171 coord
173 }
174 }
175 _ => coord,
176 };
177
178 Some(CellRef::new(cell_ref.sheet_id, adjusted_coord))
179 }
180
181 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
359pub 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
482pub 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 format!("{ast:?}")
766 }
767
768 #[test]
769 fn test_reference_adjustment_on_row_insert() {
770 let adjuster = ReferenceAdjuster::new();
771
772 let ast = parse("=A5+B10").unwrap();
774
775 let adjusted = adjuster.adjust_ast(
777 &ast,
778 &ShiftOperation::InsertRows {
779 sheet_id: 0,
780 before: 7,
781 count: 2,
782 },
783 );
784
785 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); 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); 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 let ast = parse("=C1+F1").unwrap();
813
814 let adjusted = adjuster.adjust_ast(
816 &ast,
817 &ShiftOperation::DeleteColumns {
818 sheet_id: 0,
819 start: 2, count: 2,
821 },
822 );
823
824 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); assert_eq!(*col, 4); }
843 }
844 }
845
846 #[test]
847 fn test_range_reference_adjustment() {
848 let adjuster = ReferenceAdjuster::new();
849
850 let ast = parse("=SUM(A1:A10)").unwrap();
852
853 let adjusted = adjuster.adjust_ast(
855 &ast,
856 &ShiftOperation::InsertRows {
857 sheet_id: 0,
858 before: 5,
859 count: 3,
860 },
861 );
862
863 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); assert_eq!(end_row.unwrap_or(0), 13); }
876 }
877
878 #[test]
879 fn test_relative_reference_copy() {
880 let adjuster = RelativeReferenceAdjuster::new(2, 3); let ast = parse("=A1+B2").unwrap();
884 let adjusted = adjuster.adjust_formula(&ast);
885
886 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); 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); assert_eq!(*col, 5);
903 }
904 }
905 }
906
907 #[test]
908 fn test_absolute_reference_preservation() {
909 let adjuster = ReferenceAdjuster::new();
910
911 let cell_abs_row = CellRef::new(
913 0,
914 Coord::new(5, 2, true, false), );
916
917 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 assert!(result.is_some());
929 let adjusted = result.unwrap();
930 assert_eq!(adjusted.coord.row(), 5); assert_eq!(adjusted.coord.col(), 2); 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 let cell_abs_col = CellRef::new(
942 0,
943 Coord::new(5, 2, false, true), );
945
946 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 assert!(result.is_some());
958 let adjusted = result.unwrap();
959 assert_eq!(adjusted.coord.row(), 5); assert_eq!(adjusted.coord.col(), 2); 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 let mixed1 = CellRef::new(
971 0,
972 Coord::new(5, 1, false, true), );
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); assert_eq!(adj1.coord.col(), 1); let mixed2 = CellRef::new(
991 0,
992 Coord::new(10, 3, true, false), );
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); assert_eq!(adj2.coord.col(), 2); }
1009
1010 #[test]
1011 fn test_fully_absolute_reference() {
1012 let adjuster = ReferenceAdjuster::new();
1013
1014 let fully_abs = CellRef::new(
1016 0,
1017 Coord::new(1, 1, true, true), );
1019
1020 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 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 let cell = CellRef::new(
1055 0,
1056 Coord::new(5, 3, false, false), );
1058
1059 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 assert!(result.is_none());
1071
1072 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 assert!(result2.is_none());
1084 }
1085
1086 #[test]
1087 fn test_range_expansion_on_insert() {
1088 let adjuster = ReferenceAdjuster::new();
1089
1090 let ast = parse("=SUM(B2:D10)").unwrap();
1092
1093 let adjusted = adjuster.adjust_ast(
1095 &ast,
1096 &ShiftOperation::InsertRows {
1097 sheet_id: 0,
1098 before: 5,
1099 count: 3,
1100 },
1101 );
1102
1103 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)); assert_eq!(*end_row, Some(13)); assert_eq!(*start_col, Some(2)); assert_eq!(*end_col, Some(4)); }
1122 }
1123
1124 #[test]
1125 fn test_range_contraction_on_delete() {
1126 let adjuster = ReferenceAdjuster::new();
1127
1128 let ast = parse("=SUM(A5:A20)").unwrap();
1130
1131 let adjusted = adjuster.adjust_ast(
1133 &ast,
1134 &ShiftOperation::DeleteRows {
1135 sheet_id: 0,
1136 start: 10,
1137 count: 5,
1138 },
1139 );
1140
1141 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)); assert_eq!(*end_row, Some(15)); }
1154 }
1155}