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 "fmt"
5 "regexp"
6 "strconv"
7 "strings"
8)
9
10// tokKind classifies a lexed token. Formula text itself never appears in
11// error messages built around these tokens — only positions and reasons —
12// because a formula can carry sensitive cell content.
13type tokKind int
14
15const (
16 tEOF tokKind = iota
17 tNum
18 tStr
19 tIdent
20 tRef
21 tOp
22 tLParen
23 tRParen
24 tComma
25 tColon
26)
27
28type token struct {
29 kind tokKind
30 pos int // byte offset in the source, for error reporting only
31
32 num float64
33 str string
34 ident string
35 op string
36 ref refToken
37}
38
39// refToken is a lexed single-cell reference, decoded but not yet an AST
40// node (the parser wraps it in *Ref or joins two into *Range).
41type refToken struct {
42 colPos, rowPos int
43 absCol, absRow bool
44}
45
46var refPattern = regexp.MustCompile(`^\$?[A-Za-z]{1,3}\$?[0-9]+$`)
47
48// lex scans src into a token stream terminated by a single tEOF token.
49func lex(src string) ([]token, error) {
50 var toks []token
51 i := 0
52 n := len(src)
53 for i < n {
54 c := src[i]
55 switch {
56 case c == ' ' || c == '\t' || c == '\n' || c == '\r':
57 i++
58 case isDigit(c) || (c == '.' && i+1 < n && isDigit(src[i+1])):
59 tok, next, err := lexNumber(src, i)
60 if err != nil {
61 return nil, err
62 }
63 toks = append(toks, tok)
64 i = next
65 case c == '"':
66 tok, next, err := lexString(src, i)
67 if err != nil {
68 return nil, err
69 }
70 toks = append(toks, tok)
71 i = next
72 case isLetter(c) || c == '$':
73 tok, next, err := lexWord(src, i)
74 if err != nil {
75 return nil, err
76 }
77 toks = append(toks, tok)
78 i = next
79 default:
80 tok, next, err := lexOperator(src, i)
81 if err != nil {
82 return nil, err
83 }
84 toks = append(toks, tok)
85 i = next
86 }
87 }
88 toks = append(toks, token{kind: tEOF, pos: n})
89 return toks, nil
90}
91
92func isDigit(c byte) bool { return c >= '0' && c <= '9' }
93
94func isLetter(c byte) bool {
95 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
96}
97
98func isWordChar(c byte) bool {
99 return isLetter(c) || isDigit(c) || c == '$'
100}
101
102func lexNumber(src string, start int) (token, int, error) {
103 i := start
104 n := len(src)
105 for i < n && isDigit(src[i]) {
106 i++
107 }
108 if i < n && src[i] == '.' {
109 i++
110 for i < n && isDigit(src[i]) {
111 i++
112 }
113 }
114 if i < n && (src[i] == 'e' || src[i] == 'E') {
115 j := i + 1
116 if j < n && (src[j] == '+' || src[j] == '-') {
117 j++
118 }
119 if j < n && isDigit(src[j]) {
120 i = j
121 for i < n && isDigit(src[i]) {
122 i++
123 }
124 }
125 }
126 v, err := strconv.ParseFloat(src[start:i], 64)
127 if err != nil {
128 return token{}, 0, fmt.Errorf("calc: malformed number literal at position %d", start)
129 }
130 return token{kind: tNum, pos: start, num: v}, i, nil
131}
132
133func lexString(src string, start int) (token, int, error) {
134 i := start + 1
135 n := len(src)
136 var sb strings.Builder
137 for {
138 if i >= n {
139 return token{}, 0, fmt.Errorf("calc: unterminated string literal starting at position %d", start)
140 }
141 if src[i] == '"' {
142 if i+1 < n && src[i+1] == '"' {
143 sb.WriteByte('"')
144 i += 2
145 continue
146 }
147 i++
148 break
149 }
150 sb.WriteByte(src[i])
151 i++
152 }
153 return token{kind: tStr, pos: start, str: sb.String()}, i, nil
154}
155
156// lexWord scans a maximal run of letters/digits/'$' and decides whether it
157// is a reference or an identifier (a bare word, a boolean literal, or a
158// function name). A word can match the reference shape and still be a
159// function name — e.g. LOG10 is three letters followed by digits, exactly
160// refPattern's shape — so before committing to tRef we peek past any
161// whitespace for an opening '(': a word immediately followed by a call
162// parenthesis is always a function name, never a reference.
163func lexWord(src string, start int) (token, int, error) {
164 i := start
165 n := len(src)
166 for i < n && isWordChar(src[i]) {
167 i++
168 }
169 w := src[start:i]
170
171 if refPattern.MatchString(w) && !followedByCallParen(src, i) {
172 ref, err := decodeRef(w)
173 if err != nil {
174 return token{}, 0, fmt.Errorf("calc: malformed reference at position %d", start)
175 }
176 return token{kind: tRef, pos: start, ref: ref}, i, nil
177 }
178 if strings.Contains(w, "$") {
179 return token{}, 0, fmt.Errorf("calc: malformed reference at position %d", start)
180 }
181 return token{kind: tIdent, pos: start, ident: w}, i, nil
182}
183
184// followedByCallParen reports whether the next non-whitespace character
185// starting at i is '(', which disambiguates a word like LOG10 as a
186// function name rather than a reference.
187func followedByCallParen(src string, i int) bool {
188 for i < len(src) && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
189 i++
190 }
191 return i < len(src) && src[i] == '('
192}
193
194// decodeRef splits a word already known to match refPattern into a
195// zero-based, absolute-flagged reference. A1 notation is one-based, so a
196// row literal of 0 (e.g. "A0") is malformed input, not row -1 — it is
197// rejected here rather than allowed to flow downstream as a negative
198// position.
199func decodeRef(w string) (refToken, error) {
200 idx := 0
201 var r refToken
202 if w[idx] == '$' {
203 r.absCol = true
204 idx++
205 }
206 letterStart := idx
207 for idx < len(w) && isLetter(w[idx]) {
208 idx++
209 }
210 col, err := ParseColLabel(w[letterStart:idx])
211 if err != nil {
212 return refToken{}, err
213 }
214 r.colPos = col
215 if idx < len(w) && w[idx] == '$' {
216 r.absRow = true
217 idx++
218 }
219 row, err := strconv.Atoi(w[idx:])
220 if err != nil {
221 return refToken{}, err
222 }
223 if row < 1 {
224 return refToken{}, fmt.Errorf("calc: row must be positive")
225 }
226 r.rowPos = row - 1
227 return r, nil
228}
229
230func lexOperator(src string, i int) (token, int, error) {
231 c := src[i]
232 switch c {
233 case '+', '-', '*', '/', '^', '&', '=':
234 return token{kind: tOp, pos: i, op: string(c)}, i + 1, nil
235 case '<':
236 if i+1 < len(src) && src[i+1] == '=' {
237 return token{kind: tOp, pos: i, op: "<="}, i + 2, nil
238 }
239 if i+1 < len(src) && src[i+1] == '>' {
240 return token{kind: tOp, pos: i, op: "<>"}, i + 2, nil
241 }
242 return token{kind: tOp, pos: i, op: "<"}, i + 1, nil
243 case '>':
244 if i+1 < len(src) && src[i+1] == '=' {
245 return token{kind: tOp, pos: i, op: ">="}, i + 2, nil
246 }
247 return token{kind: tOp, pos: i, op: ">"}, i + 1, nil
248 case '(':
249 return token{kind: tLParen, pos: i}, i + 1, nil
250 case ')':
251 return token{kind: tRParen, pos: i}, i + 1, nil
252 case ',':
253 return token{kind: tComma, pos: i}, i + 1, nil
254 case ':':
255 return token{kind: tColon, pos: i}, i + 1, nil
256 default:
257 return token{}, 0, fmt.Errorf("calc: unexpected character at position %d", i)
258 }
259}
260