use super::GridEdit::*;
use super::*;
fn t(formula: &str, edit: GridEdit) -> String {
shift_refs_text(formula, "Sheet1", "Sheet1", edit).unwrap()
}
#[test]
fn insert_row_above_a_cell_shifts_it_down() {
assert_eq!(t("=A5", InsertRows { at: 2, count: 1 }), "=A6");
}
#[test]
fn insert_row_below_a_cell_leaves_it_alone() {
assert_eq!(t("=A5", InsertRows { at: 6, count: 1 }), "=A5");
}
#[test]
fn insert_row_exactly_at_a_cell_shifts_it_down() {
assert_eq!(t("=A5", InsertRows { at: 5, count: 1 }), "=A6");
}
#[test]
fn insert_multiple_rows_shifts_by_the_count() {
assert_eq!(t("=A5", InsertRows { at: 1, count: 3 }), "=A8");
}
#[test]
fn insert_row_inside_a_range_expands_it() {
assert_eq!(
t("=SUM(A1:A5)", InsertRows { at: 3, count: 1 }),
"=SUM(A1:A6)"
);
}
#[test]
fn insert_row_at_a_ranges_first_row_moves_the_whole_range() {
assert_eq!(
t("=SUM(A2:A5)", InsertRows { at: 2, count: 1 }),
"=SUM(A3:A6)"
);
}
#[test]
fn insert_row_at_a_ranges_last_row_expands_it() {
assert_eq!(
t("=SUM(A1:A3)", InsertRows { at: 3, count: 1 }),
"=SUM(A1:A4)"
);
}
#[test]
fn insert_row_just_past_a_ranges_last_row_leaves_it_alone() {
assert_eq!(
t("=SUM(A1:A3)", InsertRows { at: 4, count: 1 }),
"=SUM(A1:A3)"
);
}
#[test]
fn insert_row_does_not_touch_the_column_axis() {
assert_eq!(
t("=SUM(B2:D4)", InsertRows { at: 1, count: 1 }),
"=SUM(B3:D5)"
);
}
#[test]
fn delete_row_above_a_cell_shifts_it_up() {
assert_eq!(t("=A5", DeleteRows { at: 2, count: 1 }), "=A4");
}
#[test]
fn delete_row_below_a_cell_leaves_it_alone() {
assert_eq!(t("=A5", DeleteRows { at: 6, count: 1 }), "=A5");
}
#[test]
fn delete_the_row_a_cell_points_at_makes_it_a_ref_error() {
assert_eq!(t("=A5", DeleteRows { at: 5, count: 1 }), "=#REF!");
}
#[test]
fn delete_rows_wholly_containing_a_range_makes_it_a_ref_error() {
assert_eq!(
t("=SUM(A2:A4)", DeleteRows { at: 1, count: 5 }),
"=SUM(#REF!)"
);
}
#[test]
fn delete_rows_exactly_covering_a_range_makes_it_a_ref_error() {
assert_eq!(
t("=SUM(A2:A4)", DeleteRows { at: 2, count: 3 }),
"=SUM(#REF!)"
);
}
#[test]
fn delete_a_row_inside_a_range_shrinks_it() {
assert_eq!(
t("=SUM(A1:A5)", DeleteRows { at: 3, count: 1 }),
"=SUM(A1:A4)"
);
}
#[test]
fn delete_rows_overlapping_a_ranges_start_clamps_the_start() {
assert_eq!(
t("=SUM(A2:A5)", DeleteRows { at: 1, count: 3 }),
"=SUM(A1:A2)"
);
}
#[test]
fn delete_rows_overlapping_a_ranges_end_clamps_the_end() {
assert_eq!(
t("=SUM(A1:A5)", DeleteRows { at: 3, count: 9 }),
"=SUM(A1:A2)"
);
}
#[test]
fn delete_rows_entirely_below_a_range_leaves_it_alone() {
assert_eq!(
t("=SUM(A1:A3)", DeleteRows { at: 4, count: 2 }),
"=SUM(A1:A3)"
);
}
#[test]
fn delete_rows_entirely_above_a_range_shifts_it_up() {
assert_eq!(
t("=SUM(A5:A7)", DeleteRows { at: 1, count: 2 }),
"=SUM(A3:A5)"
);
}
#[test]
fn insert_column_left_of_a_cell_shifts_it_right() {
assert_eq!(t("=C5", InsertColumns { at: 2, count: 1 }), "=D5");
}
#[test]
fn insert_column_right_of_a_cell_leaves_it_alone() {
assert_eq!(t("=C5", InsertColumns { at: 4, count: 1 }), "=C5");
}
#[test]
fn delete_the_column_a_cell_points_at_makes_it_a_ref_error() {
assert_eq!(t("=C5", DeleteColumns { at: 3, count: 1 }), "=#REF!");
}
#[test]
fn delete_a_column_inside_a_range_shrinks_it() {
assert_eq!(
t("=SUM(A1:E1)", DeleteColumns { at: 3, count: 1 }),
"=SUM(A1:D1)"
);
}
#[test]
fn delete_column_does_not_touch_the_row_axis() {
assert_eq!(
t("=SUM(B2:D4)", DeleteColumns { at: 1, count: 1 }),
"=SUM(A2:C4)"
);
}
#[test]
fn absolute_row_anchor_still_shifts_and_is_preserved() {
assert_eq!(t("=A$5", InsertRows { at: 1, count: 1 }), "=A$6");
}
#[test]
fn fully_absolute_reference_still_shifts_and_is_preserved() {
assert_eq!(t("=$A$5", InsertRows { at: 1, count: 1 }), "=$A$6");
}
#[test]
fn absolute_column_anchor_shifts_on_column_insert() {
assert_eq!(t("=$C$5", InsertColumns { at: 1, count: 1 }), "=$D$5");
}
#[test]
fn absolute_range_endpoints_are_preserved_when_shrinking() {
assert_eq!(
t("=SUM($A$1:$A$5)", DeleteRows { at: 3, count: 1 }),
"=SUM($A$1:$A$4)"
);
}
#[test]
fn qualified_reference_to_the_edited_sheet_shifts() {
assert_eq!(
t("=Sheet1!A5", InsertRows { at: 1, count: 1 }),
"=Sheet1!A6"
);
}
#[test]
fn qualified_reference_to_another_sheet_does_not_move() {
assert_eq!(t("=Other!A5", InsertRows { at: 1, count: 1 }), "=Other!A5");
}
#[test]
fn qualified_reference_to_another_sheet_is_never_deleted() {
assert_eq!(
t("=SUM(Other!A2:A4)", DeleteRows { at: 1, count: 9 }),
"=SUM(Other!A2:A4)"
);
}
#[test]
fn bare_reference_in_a_formula_on_another_sheet_does_not_move() {
let out = shift_refs_text("=A5", "Sheet2", "Sheet1", InsertRows { at: 1, count: 1 }).unwrap();
assert_eq!(out, "=A5");
}
#[test]
fn cross_sheet_formula_still_shifts_its_qualified_refs_to_the_edited_sheet() {
let out = shift_refs_text(
"=Sheet1!A5+A5",
"Sheet2",
"Sheet1",
InsertRows { at: 1, count: 1 },
)
.unwrap();
assert_eq!(out, "=Sheet1!A6+A5");
}
#[test]
fn sheet_matching_is_case_insensitive() {
let out = shift_refs_text(
"=SHEET1!A5",
"Sheet2",
"sheet1",
InsertRows { at: 1, count: 1 },
)
.unwrap();
assert_eq!(out, "=SHEET1!A6");
}
#[test]
fn quoted_sheet_name_is_requoted_on_output() {
let out = shift_refs_text(
"='Q2 Data'!A5",
"Other",
"Q2 Data",
InsertRows { at: 1, count: 1 },
)
.unwrap();
assert_eq!(out, "='Q2 Data'!A6");
}
#[test]
fn deleting_a_qualified_reference_drops_the_sheet_qualifier_with_it() {
assert_eq!(t("=Sheet1!A5", DeleteRows { at: 5, count: 1 }), "=#REF!");
}
#[test]
fn string_literals_are_left_untouched() {
assert_eq!(
t("=CONCAT(\"A5\",A5)", InsertRows { at: 1, count: 1 }),
"=CONCAT(\"A5\",A6)"
);
}
#[test]
fn function_names_and_defined_names_are_left_untouched() {
assert_eq!(
t("=SUM(A5,TAX_RATE)", InsertRows { at: 1, count: 1 }),
"=SUM(A6,TAX_RATE)"
);
}
#[test]
fn let_bound_names_that_look_like_addresses_are_left_untouched() {
assert_eq!(
t("=LET(A5, 5, A5*2)", InsertRows { at: 1, count: 1 }),
"=LET(A5, 5, A5*2)"
);
}
#[test]
fn multiple_references_splice_correctly_when_lengths_change() {
assert_eq!(t("=A1+A9", InsertRows { at: 1, count: 1 }), "=A2+A10");
}
#[test]
fn zero_count_is_a_no_op() {
assert_eq!(
t("=SUM(A1:A5)", InsertRows { at: 3, count: 0 }),
"=SUM(A1:A5)"
);
assert_eq!(
t("=SUM(A1:A5)", DeleteRows { at: 3, count: 0 }),
"=SUM(A1:A5)"
);
}
#[test]
fn index_zero_is_rejected() {
assert!(shift_refs_text("=A1", "Sheet1", "Sheet1", InsertRows { at: 0, count: 1 }).is_err());
assert!(shift_refs_text("=A1", "Sheet1", "Sheet1", DeleteRows { at: 0, count: 1 }).is_err());
}
#[test]
fn parse_errors_propagate() {
assert!(shift_refs_text("=SUM(", "Sheet1", "Sheet1", InsertRows { at: 1, count: 1 }).is_err());
}
#[test]
fn insert_that_pushes_a_reference_off_the_grid_makes_it_a_ref_error() {
let out = t("=A10000000", InsertRows { at: 1, count: 1 });
assert_eq!(out, "=#REF!");
}
#[test]
fn output_of_a_deleting_edit_re_parses() {
let out = t("=SUM(Sheet1!A2:A4)+1", DeleteRows { at: 1, count: 9 });
assert_eq!(out, "=SUM(#REF!)+1");
assert!(crate::parser::parse_formula(&out).is_ok());
}
#[test]
fn backwards_range_untouched_by_the_edit_is_left_alone() {
assert_eq!(
t("=SUM(A5:A1)", InsertRows { at: 10, count: 1 }),
"=SUM(A5:A1)"
);
assert_eq!(
t("=SUM(A5:A1)", DeleteRows { at: 10, count: 1 }),
"=SUM(A5:A1)"
);
}
#[test]
fn backwards_range_shrinks_from_the_written_end() {
assert_eq!(
t("=SUM(A5:A1)", DeleteRows { at: 4, count: 2 }),
"=SUM(A3:A1)"
);
}
#[test]
fn backwards_range_wholly_deleted_is_a_ref_error() {
assert_eq!(
t("=SUM(A5:A1)", DeleteRows { at: 1, count: 9 }),
"=SUM(#REF!)"
);
}
#[test]
fn backwards_range_shifts_whole_on_insert_above_it() {
assert_eq!(
t("=SUM(A5:A1)", InsertRows { at: 1, count: 2 }),
"=SUM(A7:A3)"
);
}
#[test]
fn backwards_column_range_shrinks_correctly() {
assert_eq!(
t("=SUM(E1:A1)", DeleteColumns { at: 4, count: 2 }),
"=SUM(C1:A1)"
);
}
#[test]
fn delete_columns_wholly_containing_a_range_makes_it_a_ref_error() {
assert_eq!(
t("=SUM(B1:D1)", DeleteColumns { at: 1, count: 9 }),
"=SUM(#REF!)"
);
}
#[test]
fn delete_columns_overlapping_a_ranges_start_clamps_the_start() {
assert_eq!(
t("=SUM(B1:E1)", DeleteColumns { at: 1, count: 3 }),
"=SUM(A1:B1)"
);
}
#[test]
fn delete_columns_overlapping_a_ranges_end_clamps_the_end() {
assert_eq!(
t("=SUM(A1:E1)", DeleteColumns { at: 3, count: 9 }),
"=SUM(A1:B1)"
);
}
#[test]
fn absolute_column_anchor_is_preserved_across_a_column_delete() {
assert_eq!(
t("=SUM($C$1:$E$1)", DeleteColumns { at: 4, count: 1 }),
"=SUM($C$1:$D$1)"
);
}
#[test]
fn insert_that_pushes_a_column_off_the_grid_makes_it_a_ref_error() {
assert_eq!(t("=ZZZ1", InsertColumns { at: 1, count: 1 }), "=#REF!");
}
#[test]
fn a_surviving_range_keeps_its_sheet_qualifier_through_a_shrink() {
assert_eq!(
t("=SUM(Sheet1!A1:A5)", DeleteRows { at: 3, count: 1 }),
"=SUM(Sheet1!A1:A4)"
);
}
#[test]
fn lambda_params_and_shadowed_body_uses_are_left_untouched() {
assert_eq!(
t("=LAMBDA(A5, A5*2)(3)", InsertRows { at: 1, count: 1 }),
"=LAMBDA(A5, A5*2)(3)"
);
}
#[test]
fn lambda_call_arguments_are_real_references_and_shift() {
assert_eq!(
t("=LAMBDA(X, X*2)(A5)", InsertRows { at: 1, count: 1 }),
"=LAMBDA(X, X*2)(A6)"
);
}
#[test]
fn a_two_dimensional_range_wholly_removed_by_a_row_delete_is_a_ref_error() {
assert_eq!(
t("=SUM(B2:D8)", DeleteRows { at: 1, count: 100 }),
"=SUM(#REF!)"
);
}
#[test]
fn a_two_dimensional_range_shifts_only_on_the_edited_axis() {
assert_eq!(
t("=SUM(B2:D8)", DeleteRows { at: 1, count: 1 }),
"=SUM(B1:D7)"
);
}
#[test]
fn a_range_with_only_one_endpoint_pushed_off_the_grid_is_a_ref_error() {
assert_eq!(
t("=SUM(A9999999:A10000000)", InsertRows { at: 1, count: 1 }),
"=SUM(#REF!)"
);
}
#[test]
fn an_index_beyond_the_axis_maximum_is_a_no_op() {
assert_eq!(
t(
"=SUM(A1:A5)",
InsertRows {
at: 20_000_000,
count: 1
}
),
"=SUM(A1:A5)"
);
assert_eq!(
t(
"=SUM(A1:A5)",
DeleteRows {
at: 20_000_000,
count: 1
}
),
"=SUM(A1:A5)"
);
}
#[test]
fn huge_counts_do_not_overflow() {
assert_eq!(
t(
"=A5",
InsertRows {
at: 1,
count: u32::MAX
}
),
"=#REF!"
);
assert_eq!(
t(
"=A5",
DeleteRows {
at: 1,
count: u32::MAX
}
),
"=#REF!"
);
assert_eq!(
t(
"=A5",
DeleteRows {
at: 6,
count: u32::MAX
}
),
"=A5"
);
}
#[test]
fn excel_flavor_is_rejected() {
let err = crate::Engine::excel()
.shift_refs_for_grid_edit("=A1", "Sheet1", "Sheet1", InsertRows { at: 1, count: 1 })
.unwrap_err();
assert!(err.message.contains("Excel flavor not yet supported"));
}
#[test]
fn engine_surface_matches_the_module_level_transform() {
let edit = DeleteRows { at: 2, count: 2 };
let out = crate::Engine::sheets()
.shift_refs_for_grid_edit("=SUM(A1:A5)+A3", "Sheet1", "Sheet1", edit)
.unwrap();
assert_eq!(out, t("=SUM(A1:A5)+A3", edit));
}
fn mv(formula: &str, mv: AxisMove) -> String {
shift_refs_for_move(formula, "Sheet1", "Sheet1", mv).unwrap()
}
#[test]
fn move_reference_unaffected_before_both_band_and_destination_is_unchanged() {
assert_eq!(
mv(
"=A1",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
),
"=A1"
);
}
#[test]
fn move_does_not_touch_a_reference_on_another_sheet() {
assert_eq!(
mv(
"=Other!A5",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
),
"=Other!A5"
);
}
#[test]
fn bare_reference_in_a_formula_on_another_sheet_is_untouched_by_a_move() {
let out = shift_refs_for_move(
"=A5",
"Sheet2",
"Sheet1",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2,
},
)
.unwrap();
assert_eq!(out, "=A5");
}
#[test]
fn move_reference_inside_the_band_translates_backward() {
assert_eq!(
mv(
"=A5",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
),
"=A2"
);
}
#[test]
fn move_reference_inside_the_band_translates_forward() {
assert_eq!(
mv(
"=A2",
AxisMove {
axis: Axis::Row,
start: 2,
end: 4,
at: 8
}
),
"=A8"
);
}
#[test]
fn move_backward_shifts_the_gap_region_forward_by_the_bands_width() {
assert_eq!(
mv(
"=A3",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
),
"=A6"
);
}
#[test]
fn move_forward_shifts_the_gap_region_backward_by_the_bands_width() {
assert_eq!(
mv(
"=A5",
AxisMove {
axis: Axis::Row,
start: 2,
end: 4,
at: 8
}
),
"=A2"
);
}
#[test]
fn canonical_example_swaps_row_three_and_row_six() {
assert_eq!(
mv(
"=A3+A6",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
),
"=A6+A3"
);
}
#[test]
fn move_induced_corner_swap_normalizes_an_ascending_range() {
assert_eq!(
mv(
"=SUM(A4:A6)",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
),
"=SUM(A3:A7)"
);
}
#[test]
fn a_range_already_written_backwards_is_not_confused_with_a_move_induced_swap() {
assert_eq!(
mv(
"=SUM(A7:A5)",
AxisMove {
axis: Axis::Row,
start: 20,
end: 22,
at: 30
}
),
"=SUM(A7:A5)"
);
}
#[test]
fn a_range_already_written_backwards_stays_backwards_when_the_move_would_uncross_it() {
assert_eq!(
mv(
"=SUM(A6:A4)",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
),
"=SUM(A7:A3)"
);
}
#[test]
fn absolute_anchors_are_preserved_through_a_move() {
assert_eq!(
mv(
"=$A$6",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
),
"=$A$3"
);
}
#[test]
fn moving_a_band_to_its_own_start_is_a_no_op() {
assert_eq!(
mv(
"=SUM(A1:A10)",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 5
}
),
"=SUM(A1:A10)"
);
}
#[test]
fn moving_a_band_to_a_destination_inside_itself_is_a_no_op() {
assert_eq!(
mv(
"=SUM(A1:A10)",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 6
}
),
"=SUM(A1:A10)"
);
}
#[test]
fn moving_a_band_to_a_destination_inside_itself_is_a_no_op_even_for_a_reference_in_the_band() {
for formula in ["=A5", "=A6", "=A7", "=A8", "=A5+A6+A7+A8"] {
assert_eq!(
mv(
formula,
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 6
}
),
formula
);
}
}
#[test]
fn moving_a_band_to_immediately_after_itself_is_a_real_move_not_a_no_op() {
assert_eq!(
mv(
"=A5+A8",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 8
}
),
"=A8+A5"
);
}
#[test]
fn column_axis_move_translates_the_band() {
assert_eq!(
mv(
"=E1",
AxisMove {
axis: Axis::Column,
start: 5,
end: 7,
at: 2
}
),
"=B1"
);
}
#[test]
fn column_axis_move_shifts_the_gap_region() {
assert_eq!(
mv(
"=C1",
AxisMove {
axis: Axis::Column,
start: 5,
end: 7,
at: 2
}
),
"=F1"
);
}
#[test]
fn a_two_dimensional_range_moves_only_on_the_moved_axis() {
assert_eq!(
mv(
"=SUM(B2:D8)",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
),
"=SUM(B5:D8)"
);
}
#[test]
fn zero_based_start_is_rejected() {
assert!(shift_refs_for_move(
"=A1",
"Sheet1",
"Sheet1",
AxisMove {
axis: Axis::Row,
start: 0,
end: 2,
at: 5
}
)
.is_err());
}
#[test]
fn zero_based_at_is_rejected() {
assert!(shift_refs_for_move(
"=A1",
"Sheet1",
"Sheet1",
AxisMove {
axis: Axis::Row,
start: 1,
end: 2,
at: 0
}
)
.is_err());
}
#[test]
fn start_greater_than_end_is_rejected() {
assert!(shift_refs_for_move(
"=A1",
"Sheet1",
"Sheet1",
AxisMove {
axis: Axis::Row,
start: 5,
end: 2,
at: 1
}
)
.is_err());
}
#[test]
fn destination_pushing_the_band_off_the_grid_is_rejected() {
assert!(shift_refs_for_move(
"=A1",
"Sheet1",
"Sheet1",
AxisMove {
axis: Axis::Row,
start: 1,
end: 2,
at: 10_000_000,
}
)
.is_err());
}
#[test]
fn at_near_u32_max_is_rejected_without_overflow() {
assert!(shift_refs_for_move(
"=A1",
"Sheet1",
"Sheet1",
AxisMove {
axis: Axis::Row,
start: 1,
end: 1,
at: u32::MAX
}
)
.is_err());
}
#[test]
fn parse_errors_propagate_for_a_move() {
assert!(shift_refs_for_move(
"=SUM(",
"Sheet1",
"Sheet1",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2
}
)
.is_err());
}
#[test]
fn output_of_a_move_re_parses() {
let out = mv(
"=SUM(A4:A6)",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2,
},
);
assert_eq!(out, "=SUM(A3:A7)");
assert!(crate::parser::parse_formula(&out).is_ok());
}
#[test]
fn engine_excel_flavor_is_rejected_for_move() {
let err = crate::Engine::excel()
.shift_refs_for_move(
"=A1",
"Sheet1",
"Sheet1",
AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2,
},
)
.unwrap_err();
assert!(err.message.contains("Excel flavor not yet supported"));
}
#[test]
fn engine_surface_matches_the_module_level_move_transform() {
let axis_move = AxisMove {
axis: Axis::Row,
start: 5,
end: 7,
at: 2,
};
let out = crate::Engine::sheets()
.shift_refs_for_move("=SUM(A4:A6)", "Sheet1", "Sheet1", axis_move)
.unwrap();
assert_eq!(out, mv("=SUM(A4:A6)", axis_move));
}