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
3// fn_math.go: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX — the aggregate
4// family. Rounding/misc math (ROUND, ROUNDUP, ROUNDDOWN, ABS, SQRT,
5// POWER, MOD) lives in fn_math_round.go; split out to stay under the
6// 300-line cap once this file grew the literal-vs-reference machinery
7// below.
8
9func init() {
10 registerNode("SUM", fnSum)
11 registerNode("AVERAGE", fnAverage)
12 registerNode("COUNT", fnCount)
13 Register("COUNTA", fnCounta)
14 registerNode("MIN", fnMin)
15 registerNode("MAX", fnMax)
16}
17
18// taggedValue pairs a value with whether it came from a cell reference —
19// a bare *Ref, or one of a *Range's flattened cells — rather than being
20// typed directly into the formula. SUM, AVERAGE, COUNT, MIN and MAX all
21// need this distinction: Excel silently ignores a blank, text, or
22// logical value read FROM a reference, but coerces (or errors on) that
23// same kind of value when it is typed DIRECTLY as an argument —
24// SUM(A1) where A1 contains TRUE is 0, but SUM(TRUE) is 1. Microsoft's
25// own documentation for SUM/AVERAGE/COUNT/MIN/MAX all use the same
26// phrase for the direct-argument case: "logical values and text
27// representations of numbers that you type directly into the list of
28// arguments are counted."
29//
30// This is why these five register as nodeFuncs (registry.go) instead of
31// ordinary Funcs: by the time an ordinary Func sees a flattened
32// []Value, a reference's cells and a literal argument are
33// indistinguishable — the exact gap flagged, but not yet closed, the
34// first time this file was written.
35type taggedValue struct {
36 Value
37 fromRef bool
38}
39
40// evalAggArgs evaluates a call's argument nodes into taggedValues. A bare
41// *Ref and each cell of a *Range are both tagged fromRef=true — Excel
42// treats a single-cell reference the same as a one-cell range for this
43// purpose, not like a literal. Everything else (a number/string/bool
44// literal, or any computed expression such as an arithmetic operation or
45// a nested call) evaluates through Eval and is tagged fromRef=false.
46func evalAggArgs(nodes []Node, env Env) []taggedValue {
47 var out []taggedValue
48 for _, n := range nodes {
49 switch t := n.(type) {
50 case *Range:
51 cells, ok := rangeCells(t)
52 if !ok {
53 out = append(out, taggedValue{Value{Kind: KindErr, Err: ErrRef}, true})
54 continue
55 }
56 for _, k := range cells {
57 out = append(out, taggedValue{env.CellValue(k), true})
58 }
59 case *Ref:
60 out = append(out, taggedValue{env.CellValue(refKey(t)), true})
61 default:
62 out = append(out, taggedValue{Eval(n, env), false})
63 }
64 }
65 return out
66}
67
68// numericValues applies to SUM, AVERAGE, MIN and MAX. Any KindErr value
69// propagates immediately, left-most wins — matching every other error
70// path in this package. A value read from a reference (fromRef=true)
71// counts only if it is already KindNum: a blank, a text cell, or a
72// logical cell is silently skipped, matching how Excel treats a range's
73// own blank/text/logical cells. A value typed directly as an argument
74// (fromRef=false) additionally coerces a KindBool to 1/0 and a KindStr
75// that parses as a number — Excel's own "typed directly into the list of
76// arguments" rule (see taggedValue's doc comment). A literal string that
77// does NOT parse as a number is #VALUE! — unlike a range's text cell, a
78// literal you plainly meant to be numeric is not silently ignored.
79func numericValues(args []taggedValue) ([]float64, *Value) {
80 var nums []float64
81 for i := range args {
82 a := args[i]
83 if a.Kind == KindErr {
84 v := a.Value
85 return nil, &v
86 }
87 if a.fromRef {
88 if a.Kind == KindNum {
89 nums = append(nums, a.Num)
90 }
91 continue
92 }
93 switch a.Kind {
94 case KindNum:
95 nums = append(nums, a.Num)
96 case KindBool:
97 if a.Bool {
98 nums = append(nums, 1)
99 } else {
100 nums = append(nums, 0)
101 }
102 case KindStr:
103 n, ek := toNumber(a.Value)
104 if ek != ErrNone {
105 v := errValue(ErrValue)
106 return nil, &v
107 }
108 nums = append(nums, n)
109 default:
110 // KindEmpty typed directly: unreachable in practice — the
111 // only source of KindEmpty is an untouched cell, and every
112 // cell-reference path is already tagged fromRef=true above
113 // (evalAggArgs) — kept only for defensiveness.
114 }
115 }
116 return nums, nil
117}
118
119func fnSum(argNodes []Node, env Env) Value {
120 nums, errv := numericValues(evalAggArgs(argNodes, env))
121 if errv != nil {
122 return *errv
123 }
124 var total float64
125 for _, n := range nums {
126 total += n
127 }
128 return Value{Kind: KindNum, Num: total}
129}
130
131func fnAverage(argNodes []Node, env Env) Value {
132 nums, errv := numericValues(evalAggArgs(argNodes, env))
133 if errv != nil {
134 return *errv
135 }
136 if len(nums) == 0 {
137 return errValue(ErrDiv0)
138 }
139 var total float64
140 for _, n := range nums {
141 total += n
142 }
143 return Value{Kind: KindNum, Num: total / float64(len(nums))}
144}
145
146// fnCount counts numeric values: a KindNum from anywhere, plus — per
147// numericValues' doc comment on the same literal-vs-reference
148// distinction — a KindBool or a numeric-looking KindStr typed DIRECTLY
149// as an argument (COUNT(1,2,"a",TRUE) is 3: "a" doesn't parse as a
150// number and is skipped, but TRUE does count because it was typed
151// directly, not read from a reference). Unlike SUM/AVERAGE/MIN/MAX, no
152// argument here propagates an error — COUNT is a tally, and a broken
153// cell elsewhere in a range is common enough (a VLOOKUP helper column
154// full of #N/A, say) that COUNT/COUNTA are relied on to scan past it
155// rather than becoming it.
156func fnCount(argNodes []Node, env Env) Value {
157 n := 0
158 for _, a := range evalAggArgs(argNodes, env) {
159 if a.Kind == KindErr {
160 continue
161 }
162 if a.fromRef {
163 if a.Kind == KindNum {
164 n++
165 }
166 continue
167 }
168 switch a.Kind {
169 case KindNum, KindBool:
170 n++
171 case KindStr:
172 if _, ek := toNumber(a.Value); ek == ErrNone {
173 n++
174 }
175 }
176 }
177 return Value{Kind: KindNum, Num: float64(n)}
178}
179
180// fnCounta counts every argument that is not KindEmpty — numbers,
181// strings, booleans and errors all count as "present," from either a
182// reference or a literal alike, which is why (unlike its four siblings
183// above) COUNTA needs no literal-vs-reference distinction and stays an
184// ordinary Func. This is the property COUNT vs. COUNTA hinges on (task
185// 8's brief names it directly): neither counts a blank cell, but COUNTA
186// counts everything else that COUNT would skip.
187func fnCounta(args []Value) Value {
188 n := 0
189 for _, a := range args {
190 if a.Kind != KindEmpty {
191 n++
192 }
193 }
194 return Value{Kind: KindNum, Num: float64(n)}
195}
196
197func fnMin(argNodes []Node, env Env) Value {
198 nums, errv := numericValues(evalAggArgs(argNodes, env))
199 if errv != nil {
200 return *errv
201 }
202 if len(nums) == 0 {
203 return Value{Kind: KindNum, Num: 0}
204 }
205 m := nums[0]
206 for _, n := range nums[1:] {
207 if n < m {
208 m = n
209 }
210 }
211 return Value{Kind: KindNum, Num: m}
212}
213
214func fnMax(argNodes []Node, env Env) Value {
215 nums, errv := numericValues(evalAggArgs(argNodes, env))
216 if errv != nil {
217 return *errv
218 }
219 if len(nums) == 0 {
220 return Value{Kind: KindNum, Num: 0}
221 }
222 m := nums[0]
223 for _, n := range nums[1:] {
224 if n > m {
225 m = n
226 }
227 }
228 return Value{Kind: KindNum, Num: m}
229}
230