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 "strconv"
4
5// Translate offsets every relative reference in n by (dRow, dCol) and
6// returns a new tree. Absolute components are untouched — that is the whole
7// meaning of the dollar sign.
8//
9// This is the pass stable row and column identity does NOT remove. Ids make
10// insert and delete free; copy and fill are positional by definition, so the
11// offset has to be applied at paste time. See spec section 4.2.
12func Translate(n Node, dRow, dCol int) Node {
13 switch t := n.(type) {
14 case *Ref:
15 out := *t
16 if !out.AbsRow {
17 out.RowPos += dRow
18 }
19 if !out.AbsCol {
20 out.ColPos += dCol
21 }
22 return &out
23 case *Range:
24 return &Range{
25 From: Translate(t.From, dRow, dCol).(*Ref),
26 To: Translate(t.To, dRow, dCol).(*Ref),
27 }
28 case *Binary:
29 return &Binary{Op: t.Op,
30 Left: Translate(t.Left, dRow, dCol),
31 Right: Translate(t.Right, dRow, dCol)}
32 case *Unary:
33 return &Unary{Op: t.Op, X: Translate(t.X, dRow, dCol)}
34 case *Call:
35 args := make([]Node, len(t.Args))
36 for i, a := range t.Args {
37 args[i] = Translate(a, dRow, dCol)
38 }
39 return &Call{Name: t.Name, Args: args}
40 default:
41 return n // Num, Str, Bool have no references
42 }
43}
44
45// TranslateChecked is Translate plus the bounds check its callers cannot
46// perform afterwards, and the reason it exists is that Render is happy to
47// render a reference that does not exist.
48//
49// A relative reference can be offset off the top or the left of the grid:
50// =A1 copied one column left has nowhere to point, and Excel writes #REF!
51// there. Ours did something worse. ColLabel(-1) returns the EMPTY STRING
52// and RowPos+1 is 0, so Translate followed by Render turned `=A1` into `=1`
53// — a formula replaced by the number one, which reads as a value somebody
54// typed and is the quiet kind of wrong nothing downstream can detect.
55//
56// So the check belongs beside the translation rather than at the call site:
57// the caller has the source text and the offset, not the tree, and by the
58// time it has a string again the evidence is gone.
59//
60// ok is false if ANY reference landed off-grid, including one nested in a
61// range or a call's argument. The tree is still returned, for a caller that
62// wants to report which part failed; nothing should render it.
63func TranslateChecked(n Node, dRow, dCol int) (Node, bool) {
64 out := Translate(n, dRow, dCol)
65 return out, onGrid(out)
66}
67
68// maxSheetRows and maxSheetCols are the spreadsheet grid's own extent —
69// 1,048,576 rows and 16,384 columns (XFD), which is what Excel and
70// LibreOffice both stop at and what XLSX can represent.
71//
72// They are here rather than in the sheets package because they are a
73// property of the FORMAT, not of this deployment's window: a formula
74// translated past XFD is not a reference we happen not to store, it is
75// one that cannot be written down. Excel answers #REF! and so must we.
76const (
77 maxSheetRows = 1 << 20
78 maxSheetCols = 16384
79)
80
81// onGrid reports whether every reference in n lands inside the
82// spreadsheet grid — not merely at a non-negative position. It walks the same shapes Translate does, and deliberately in the
83// same order, so a node type added to one and not the other is a compile
84// error in Translate and a silently unchecked branch here — which is why
85// the default case returns true only for the leaf types that hold no
86// reference at all.
87func onGrid(n Node) bool {
88 switch t := n.(type) {
89 case *Ref:
90 // Both ends. The first version checked only >= 0, so copying
91 // =XFD1 one column right rendered =XFE1 — a column label the
92 // parser will happily read back and no spreadsheet can hold.
93 // Caught by the branch review.
94 return t.RowPos >= 0 && t.ColPos >= 0 &&
95 t.RowPos < maxSheetRows && t.ColPos < maxSheetCols
96 case *Range:
97 return onGrid(t.From) && onGrid(t.To)
98 case *Binary:
99 return onGrid(t.Left) && onGrid(t.Right)
100 case *Unary:
101 return onGrid(t.X)
102 case *Call:
103 for _, a := range t.Args {
104 if !onGrid(a) {
105 return false
106 }
107 }
108 return true
109 default:
110 return true // Num, Str, Bool have no references
111 }
112}
113
114// Render is the inverse of Parse for the node shapes Parse can produce:
115// numbers, strings (re-quoted with `"` doubled), booleans, references and
116// ranges (with both absolute markers preserved in position), unary minus,
117// binary operators, and upper-cased function calls. It does not reproduce
118// the source's original whitespace or its exact parenthesisation — a
119// redundant paren the source didn't need is never replayed — but for every
120// tree Parse can produce, Parse(Render(n)) reproduces a tree equal to n:
121// Render inserts parentheses wherever precedence or associativity would
122// otherwise regroup the expression, the mirror of the precedence-climbing
123// parser above. This is also what the formula bar shows, so both
124// guarantees matter beyond the tests: a Render that dropped a "$" would
125// silently turn an absolute reference into a relative one the next time it
126// was copied, and a Render that dropped a needed paren would silently
127// change the value a cell computes.
128func Render(n Node) string {
129 switch t := n.(type) {
130 case *Num:
131 return strconv.FormatFloat(t.V, 'g', -1, 64)
132 case *Str:
133 return `"` + quoteStr(t.V) + `"`
134 case *Bool:
135 if t.V {
136 return "TRUE"
137 }
138 return "FALSE"
139 case *Ref:
140 return renderRef(t)
141 case *Range:
142 return renderRef(t.From) + ":" + renderRef(t.To)
143 case *Unary:
144 return t.Op + renderOperand(t.X, precUnary, false)
145 case *Binary:
146 p := binaryPrec(t.Op)
147 return renderOperand(t.Left, p, false) + t.Op + renderOperand(t.Right, p, true)
148 case *Call:
149 args := make([]string, len(t.Args))
150 for i, a := range t.Args {
151 args[i] = Render(a)
152 }
153 return t.Name + "(" + joinComma(args) + ")"
154 default:
155 return ""
156 }
157}
158
159// Precedence levels, loosest to tightest, matching Parse's grammar
160// (comparison, concat, additive, term, power, then unary — see the
161// precedence note on Parse). Atoms (numbers, strings, refs, ranges,
162// calls) never need parenthesising as anyone's child, so they sort above
163// even unary.
164const (
165 precComparison = iota + 1
166 precConcat
167 precAdditive
168 precTerm
169 precPower
170 precUnary
171 precAtom
172)
173
174// binaryPrec maps a Binary's operator to its precedence level.
175func binaryPrec(op string) int {
176 switch op {
177 case "=", "<>", "<", "<=", ">", ">=":
178 return precComparison
179 case "&":
180 return precConcat
181 case "+", "-":
182 return precAdditive
183 case "*", "/":
184 return precTerm
185 case "^":
186 return precPower
187 default:
188 return precAtom
189 }
190}
191
192// nodePrec reports the precedence level a node binds at when it appears as
193// someone else's operand: a Binary's by its operator, a Unary's fixed at
194// precUnary (it parses tighter than any binary operator but looser than
195// nothing — see the -2^2 = 4 note on Parse), everything else at precAtom.
196func nodePrec(n Node) int {
197 switch t := n.(type) {
198 case *Binary:
199 return binaryPrec(t.Op)
200 case *Unary:
201 return precUnary
202 default:
203 return precAtom
204 }
205}
206
207// renderOperand renders n as a child sitting in a slot of precedence
208// parentPrec, adding parentheses exactly when omitting them would let the
209// child regroup under re-parsing: when it binds strictly looser than the
210// parent, or when it binds at exactly the parent's precedence on the side
211// associativity doesn't protect (see samePrecedenceNeedsParens).
212func renderOperand(n Node, parentPrec int, isRight bool) string {
213 cp := nodePrec(n)
214 needParens := cp < parentPrec || (cp == parentPrec && samePrecedenceNeedsParens(parentPrec, isRight))
215 s := Render(n)
216 if needParens {
217 return "(" + s + ")"
218 }
219 return s
220}
221
222// samePrecedenceNeedsParens decides, for a child at exactly its parent's
223// precedence, whether it still needs parens to reparse into the same tree.
224// Every level Parse builds is left-associative except ^, so a
225// left-associative parent needs parens around a same-precedence RIGHT
226// child (8-(3-1) must keep its parens: "8-3-1" reparses as (8-3)-1), while
227// the right-associative power needs them around a same-precedence LEFT
228// child instead ((2^3)^2 must keep its parens: "2^3^2" reparses as
229// 2^(3^2)). The unary slot (parentPrec == precUnary, always called with
230// isRight == false) falls into the left-associative branch, which is also
231// the answer it wants: chained unary minus ("--A1") never needs parens.
232func samePrecedenceNeedsParens(parentPrec int, isRight bool) bool {
233 if parentPrec == precPower {
234 return !isRight
235 }
236 return isRight
237}
238
239// renderRef reproduces a single reference's A1 text, including whichever
240// dollar signs are set: neither ($A1 has one, A$1 has the other, $A$1 has
241// both), matching how decodeRef reads them back.
242func renderRef(r *Ref) string {
243 s := ""
244 if r.AbsCol {
245 s += "$"
246 }
247 s += ColLabel(r.ColPos)
248 if r.AbsRow {
249 s += "$"
250 }
251 s += strconv.Itoa(r.RowPos + 1)
252 return s
253}
254
255// quoteStr doubles embedded double quotes, the inverse of lexString's
256// unescaping.
257func quoteStr(s string) string {
258 out := make([]byte, 0, len(s))
259 for i := 0; i < len(s); i++ {
260 if s[i] == '"' {
261 out = append(out, '"', '"')
262 continue
263 }
264 out = append(out, s[i])
265 }
266 return string(out)
267}
268
269func joinComma(parts []string) string {
270 out := ""
271 for i, p := range parts {
272 if i > 0 {
273 out += ","
274 }
275 out += p
276 }
277 return out
278}
279