oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package calc
2
3import (
4 "reflect"
5 "testing"
6)
7
8func 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
16func 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
24func 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
35func 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.
49func 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.
80func 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.
134func 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