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 "math"
5 "strconv"
6 "strings"
7)
8
9// evalBinary evaluates both operands unconditionally (formulas have no
10// side effects to order), then propagates the left operand's error if it
11// is one, else the right's — "left-most wins" when both sides are errors.
12func evalBinary(t *Binary, env Env) Value {
13 l := Eval(t.Left, env)
14 r := Eval(t.Right, env)
15 if l.Kind == KindErr {
16 return l
17 }
18 if r.Kind == KindErr {
19 return r
20 }
21 switch t.Op {
22 case "+", "-", "*", "/", "^":
23 return evalArith(t.Op, l, r)
24 case "&":
25 return evalConcat(l, r)
26 default: // "=", "<>", "<", "<=", ">", ">="
27 return evalCompare(t.Op, l, r)
28 }
29}
30
31// evalArith coerces both operands to numbers and applies op. Coercion
32// failure on the left operand wins over one on the right, keeping
33// "left-most wins" consistent even below the Value level.
34func evalArith(op string, l, r Value) Value {
35 ln, errKind := toNumber(l)
36 if errKind != ErrNone {
37 return Value{Kind: KindErr, Err: errKind}
38 }
39 rn, errKind := toNumber(r)
40 if errKind != ErrNone {
41 return Value{Kind: KindErr, Err: errKind}
42 }
43 switch op {
44 case "+":
45 return Value{Kind: KindNum, Num: ln + rn}
46 case "-":
47 return Value{Kind: KindNum, Num: ln - rn}
48 case "*":
49 return Value{Kind: KindNum, Num: ln * rn}
50 case "/":
51 if rn == 0 {
52 return Value{Kind: KindErr, Err: ErrDiv0}
53 }
54 return Value{Kind: KindNum, Num: ln / rn}
55 default: // "^"
56 return Value{Kind: KindNum, Num: math.Pow(ln, rn)}
57 }
58}
59
60// evalConcat implements "&": both operands render as text (Excel's own
61// coercion — TRUE, FALSE, and a number's plain decimal form) and join.
62func evalConcat(l, r Value) Value {
63 return Value{Kind: KindStr, Str: toStr(l) + toStr(r)}
64}
65
66// evalCompare implements the six comparison operators over compareValues'
67// ordering.
68func evalCompare(op string, l, r Value) Value {
69 c := compareValues(l, r)
70 var result bool
71 switch op {
72 case "=":
73 result = c == 0
74 case "<>":
75 result = c != 0
76 case "<":
77 result = c < 0
78 case "<=":
79 result = c <= 0
80 case ">":
81 result = c > 0
82 default: // ">="
83 result = c >= 0
84 }
85 return Value{Kind: KindBool, Bool: result}
86}
87
88// compareValues orders l against r the way Excel's comparison operators
89// do. A blank cell first coerces to whatever "nothing" means for the
90// *other* side's kind — 0 against a number, "" against text, FALSE
91// against a boolean, so blank = 0, blank = "", and blank = FALSE all read
92// TRUE, matching Excel — and two blanks compare equal outright. Neither
93// substitution changes the Value stored anywhere; it only affects this
94// one comparison, which is why an empty cell still counts as empty to
95// COUNT/COUNTA (task 8) even though "=" treats it as equal to 0.
96//
97// Once neither side is empty: within the same kind, numbers compare
98// numerically, text compares case-insensitively (Excel's default —
99// "abc" = "ABC"), and booleans order false before true. Across kinds,
100// every number sorts below every piece of text, and every piece of text
101// sorts below every boolean, regardless of the text's literal content —
102// the string "9" is still greater than the number 100, because it is
103// text, not because of what it says.
104func compareValues(l, r Value) int {
105 if l.Kind == KindEmpty && r.Kind == KindEmpty {
106 return 0
107 }
108 if l.Kind == KindEmpty {
109 l = emptyAs(r.Kind)
110 }
111 if r.Kind == KindEmpty {
112 r = emptyAs(l.Kind)
113 }
114 lr, rr := typeRank(l), typeRank(r)
115 if lr != rr {
116 if lr < rr {
117 return -1
118 }
119 return 1
120 }
121 switch l.Kind {
122 case KindStr:
123 return strings.Compare(strings.ToUpper(l.Str), strings.ToUpper(r.Str))
124 case KindBool:
125 switch {
126 case l.Bool == r.Bool:
127 return 0
128 case !l.Bool:
129 return -1
130 default:
131 return 1
132 }
133 default: // KindNum
134 switch {
135 case l.Num < r.Num:
136 return -1
137 case l.Num > r.Num:
138 return 1
139 default:
140 return 0
141 }
142 }
143}
144
145// emptyAs returns the identity value a blank cell coerces to when
146// compared against a cell of kind k, so compareValues never has to rank
147// KindEmpty itself against KindNum/KindStr/KindBool.
148func emptyAs(k ValueKind) Value {
149 switch k {
150 case KindStr:
151 return Value{Kind: KindStr, Str: ""}
152 case KindBool:
153 return Value{Kind: KindBool, Bool: false}
154 default: // KindNum (KindEmpty vs KindEmpty is handled before this is called)
155 return Value{Kind: KindNum, Num: 0}
156 }
157}
158
159// typeRank orders the value kinds for cross-kind comparison: number,
160// then text, then boolean. Never called with KindEmpty — compareValues
161// substitutes it away first via emptyAs.
162func typeRank(v Value) int {
163 switch v.Kind {
164 case KindStr:
165 return 1
166 case KindBool:
167 return 2
168 default: // KindNum
169 return 0
170 }
171}
172
173// toNumber coerces v for an arithmetic operator: an empty cell as 0
174// (Excel's own arithmetic coercion for a blank), a number as itself, a
175// boolean as 1/0, and text only if it parses as a number outright (no
176// currency symbols, percent signs or thousands separators — that
177// normalization belongs to whatever task parses user input into a
178// formula, not to evaluation). Never called on a KindErr Value: callers
179// check for an error operand before coercing.
180func toNumber(v Value) (float64, ErrKind) {
181 switch v.Kind {
182 case KindEmpty:
183 return 0, ErrNone
184 case KindNum:
185 return v.Num, ErrNone
186 case KindBool:
187 if v.Bool {
188 return 1, ErrNone
189 }
190 return 0, ErrNone
191 case KindStr:
192 n, err := strconv.ParseFloat(strings.TrimSpace(v.Str), 64)
193 if err != nil {
194 return 0, ErrValue
195 }
196 return n, ErrNone
197 default:
198 return 0, ErrValue
199 }
200}
201
202// toStr renders v as text for "&" concatenation: an empty cell as "" (not
203// "0" — this is the whole reason KindEmpty exists as its own kind rather
204// than collapsing into KindNum{0}), a string as itself, a number in its
205// plain decimal form (no thousands separator or currency — display
206// formatting is a concern outside this package), a boolean as TRUE/FALSE.
207func toStr(v Value) string {
208 switch v.Kind {
209 case KindEmpty:
210 return ""
211 case KindStr:
212 return v.Str
213 case KindBool:
214 if v.Bool {
215 return "TRUE"
216 }
217 return "FALSE"
218 default: // KindNum
219 return strconv.FormatFloat(v.Num, 'g', -1, 64)
220 }
221}
222