| 1 | package calc |
| 2 | |
| 3 | import ( |
| 4 | "reflect" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | func TestTranslateRelativeRef(t *testing.T) { |
| 9 | n, _ := Parse("A1") |
| 10 | got := Translate(n, 1, 0).(*Ref) // copied down one row |
| 11 | if got.RowPos != 1 || got.ColPos != 0 { |
| 12 | t.Fatalf("=A1 copied down = row %d col %d, want 1,0", got.RowPos, got.ColPos) |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | func TestTranslateLeavesAbsoluteAlone(t *testing.T) { |
| 17 | n, _ := Parse("$A$1") |
| 18 | got := Translate(n, 5, 5).(*Ref) |
| 19 | if got.RowPos != 0 || got.ColPos != 0 { |
| 20 | t.Fatal("$A$1 moved under translation") |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | func TestTranslateMixedRef(t *testing.T) { |
| 25 | n, _ := Parse("$A1") // absolute column, relative row |
| 26 | got := Translate(n, 3, 4).(*Ref) |
| 27 | if got.ColPos != 0 { |
| 28 | t.Fatal("$A moved") |
| 29 | } |
| 30 | if got.RowPos != 3 { |
| 31 | t.Fatalf("relative row = %d, want 3", got.RowPos) |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | func TestTranslateDescendsIntoCalls(t *testing.T) { |
| 36 | n, _ := Parse("SUM(A1:A5)+$B$1") |
| 37 | got := Translate(n, 2, 0).(*Binary) |
| 38 | rng := got.Left.(*Call).Args[0].(*Range) |
| 39 | if rng.From.RowPos != 2 || rng.To.RowPos != 6 { |
| 40 | t.Fatalf("range = %d:%d, want 2:6", rng.From.RowPos, rng.To.RowPos) |
| 41 | } |
| 42 | if got.Right.(*Ref).RowPos != 0 { |
| 43 | t.Fatal("$B$1 moved") |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | // Each row restates spec section 4.2. If an implementation changes one of |
| 48 | // these behaviours, this table is what it has to argue with. |
| 49 | func TestReferenceBehaviourPerOperation(t *testing.T) { |
| 50 | cases := []struct { |
| 51 | op, src, want string |
| 52 | dRow, dCol int |
| 53 | }{ |
| 54 | {op: "copy down", src: "A1", dRow: 1, want: "A2"}, |
| 55 | {op: "copy right", src: "A1", dCol: 1, want: "B1"}, |
| 56 | {op: "copy absolute", src: "$A$1", dRow: 9, dCol: 9, want: "$A$1"}, |
| 57 | {op: "fill range", src: "SUM(A1:A3)", dRow: 1, want: "SUM(A2:A4)"}, |
| 58 | } |
| 59 | for _, tc := range cases { |
| 60 | n, err := Parse(tc.src) |
| 61 | if err != nil { |
| 62 | t.Fatalf("%s: parse %q: %v", tc.op, tc.src, err) |
| 63 | } |
| 64 | if got := Render(Translate(n, tc.dRow, tc.dCol)); got != tc.want { |
| 65 | t.Errorf("%s: %q -> %q, want %q", tc.op, tc.src, got, tc.want) |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | // TestRenderRoundTripsThroughReparse pins Render's actual contract: not |
| 71 | // that its output string matches some expected literal, but that |
| 72 | // re-parsing it reproduces a tree equal to the one Render started from. |
| 73 | // String equality would be too strong (Render need not replay redundant |
| 74 | // source parens) and too weak (it would miss a wrong-but-plausible-looking |
| 75 | // rendering); re-parsing and comparing trees is the property that |
| 76 | // determines what a copied formula actually computes. Each case is a |
| 77 | // precedence or associativity composition the four-row contract table |
| 78 | // above never exercises, so a regression here would have passed that |
| 79 | // table silently. |
| 80 | func TestRenderRoundTripsThroughReparse(t *testing.T) { |
| 81 | cases := []struct { |
| 82 | name, src string |
| 83 | }{ |
| 84 | // A looser-binding left child of a tighter parent: without |
| 85 | // parens "1+2*3" reparses to 1+(2*3), a different tree — 9 |
| 86 | // would silently become 7. |
| 87 | {"parenthesized_addition_inside_multiplication", "(1+2)*3"}, |
| 88 | // Parse only ever puts a Binary directly under a Unary via |
| 89 | // source parens (parseUnary's operand otherwise recurses |
| 90 | // through parseUnary/parsePrimary, never through a binary |
| 91 | // level); without parens "-A1+A2" reparses as (-A1)+A2 — for |
| 92 | // A1=1, A2=2 that is -3 silently becoming 1. |
| 93 | {"negated_parenthesized_sum", "-(A1+A2)"}, |
| 94 | // Left-associative operators need parens around a |
| 95 | // same-precedence RIGHT operand: without them "8-3-1" |
| 96 | // reparses as (8-3)-1 — 6 would silently become 4. |
| 97 | {"left_associative_subtraction_needs_right_parens", "8-(3-1)"}, |
| 98 | // ^ is the one right-associative level, so it needs parens on |
| 99 | // a same-precedence LEFT child instead of the right: without |
| 100 | // them "2^3^2" reparses as 2^(3^2) — 64 would silently become |
| 101 | // 512. |
| 102 | {"right_associative_power_needs_left_parens", "(2^3)^2"}, |
| 103 | // The mirror case: a right child at the same precedence as its |
| 104 | // right-associative parent must NOT gain parens — this is the |
| 105 | // branch with the least test coverage, and the one where a |
| 106 | // wrong or deleted associativity check would go unnoticed. |
| 107 | {"right_associative_power_keeps_bare_right_child", "2^3^2"}, |
| 108 | } |
| 109 | for _, tc := range cases { |
| 110 | t.Run(tc.name, func(t *testing.T) { |
| 111 | want, err := Parse(tc.src) |
| 112 | if err != nil { |
| 113 | t.Fatalf("parse %q: %v", tc.src, err) |
| 114 | } |
| 115 | rendered := Render(want) |
| 116 | got, err := Parse(rendered) |
| 117 | if err != nil { |
| 118 | t.Fatalf("reparse %q (rendered from %q): %v", rendered, tc.src, err) |
| 119 | } |
| 120 | if !reflect.DeepEqual(want, got) { |
| 121 | t.Fatalf("%q rendered as %q, which reparses to a different tree:\nwant %#v\ngot %#v", tc.src, rendered, want, got) |
| 122 | } |
| 123 | }) |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | // TestTranslateCheckedRefusesPastTheGridEdge. |
| 128 | // |
| 129 | // The first version of the bounds check tested only RowPos >= 0 and |
| 130 | // ColPos >= 0, so a reference translated past the LAST column came back |
| 131 | // ok and Render printed a column label — =XFD1 copied one column right |
| 132 | // rendered as =XFE1, which the parser reads back happily and no |
| 133 | // spreadsheet can hold. Caught by the branch review, not by any test. |
| 134 | func TestTranslateCheckedRefusesPastTheGridEdge(t *testing.T) { |
| 135 | for _, tc := range []struct { |
| 136 | src string |
| 137 | dRow, dCol int |
| 138 | wantOK bool |
| 139 | }{ |
| 140 | {"XFD1", 0, 1, false}, // one column past the last |
| 141 | {"XFC1", 0, 1, true}, // onto the last column, which is fine |
| 142 | {"A1048576", 1, 0, false}, // one row past the last |
| 143 | {"A1048575", 1, 0, true}, |
| 144 | {"A1", -1, 0, false}, // off the top, the case that already worked |
| 145 | {"A1", 0, -1, false}, // off the left |
| 146 | {"SUM(A1:XFD1)", 0, 1, false}, |
| 147 | {"$XFD$1", 0, 1, true}, // absolute: it does not move, so it stays legal |
| 148 | } { |
| 149 | n, err := Parse(tc.src) |
| 150 | if err != nil { |
| 151 | t.Fatalf("%s: %v", tc.src, err) |
| 152 | } |
| 153 | _, ok := TranslateChecked(n, tc.dRow, tc.dCol) |
| 154 | if ok != tc.wantOK { |
| 155 | t.Errorf("TranslateChecked(%s, %d, %d) ok = %v, want %v", |
| 156 | tc.src, tc.dRow, tc.dCol, ok, tc.wantOK) |
| 157 | } |
| 158 | } |
| 159 | } |
| 160 | |