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_cond_apply.go: the six conditional aggregates themselves. The
4// criteria machinery they share is in fn_cond.go; split for the 300-line
5// cap, and along the seam that already existed — one file decides
6// whether a cell matches, the other decides what to do about it.
7
8// condPair is one criteria range and the criterion applied to it.
9type condPair struct {
10 over rect
11 crit criterion
12}
13
14// selected walks the first pair's rectangle and calls fn for every
15// offset where EVERY pair matches.
16//
17// Offsets, not addresses. Each further criteria range is reshaped to the
18// first one's dimensions (offsetRect), so ranges of different written
19// sizes still line up cell for cell — Excel's rule, and the one that
20// makes SUMIFS over ranges somebody wrote inconsistently give the answer
21// they expect rather than #VALUE!.
22func selected(pairs []condPair, env Env, fn func(dr, dc int)) {
23 base := pairs[0].over
24 for dr := 0; dr <= base.r1-base.r0; dr++ {
25 for dc := 0; dc <= base.c1-base.c0; dc++ {
26 all := true
27 for _, p := range pairs {
28 at := offsetRect(p.over, base)
29 if !p.crit.matches(cellValueAt(env, at.r0+dr, at.c0+dc)) {
30 all = false
31 break
32 }
33 }
34 if all {
35 fn(dr, dc)
36 }
37 }
38 }
39}
40
41// readPairs decodes (range, criteria) argument couples starting at
42// argNodes[from], stepping two at a time.
43//
44// ok is false for a bad count, a non-range where a range must be, or a
45// range over the cell cap — the same three refusals fn_lookup.go makes,
46// answered the same way, because a caller cannot tell them apart and
47// should not have to.
48func readPairs(argNodes []Node, from int, env Env) ([]condPair, Value, bool) {
49 rest := argNodes[from:]
50 if len(rest) == 0 || len(rest)%2 != 0 {
51 return nil, errValue(ErrValue), false
52 }
53 var pairs []condPair
54 for i := 0; i < len(rest); i += 2 {
55 box, ok := rectOf(rest[i])
56 if !ok {
57 return nil, errValue(ErrValue), false
58 }
59 c := Eval(rest[i+1], env)
60 if c.Kind == KindErr {
61 return nil, c, false
62 }
63 pairs = append(pairs, condPair{over: box, crit: parseCriterion(c)})
64 }
65 // Every criteria range must have the same SHAPE as the first —
66 // width and height, not area.
67 //
68 // Area was the first version of this check and it was wrong in the
69 // way that matters: A1:A3 is 3x1 and A1:C1 is 1x3, so the two have
70 // the same cell count, the pair was accepted, and offsetRect
71 // reshaped one onto the other. The result was a plausible number
72 // read out of cells nobody named — COUNTIFS(A1:A3,">5",A1:C1,"<>")
73 // answered 3. Caught by the branch review; LibreOffice answers
74 // Err:502 to that expression and excelize #VALUE!, so both oracles
75 // say refuse.
76 for _, p := range pairs[1:] {
77 if p.over.r1-p.over.r0 != pairs[0].over.r1-pairs[0].over.r0 ||
78 p.over.c1-p.over.c0 != pairs[0].over.c1-pairs[0].over.c0 {
79 return nil, errValue(ErrValue), false
80 }
81 }
82 return pairs, Value{}, true
83}
84
85// total adds up the values at the selected offsets of `over`, counts
86// them, and reports the first error it met, so SUM and AVERAGE share one
87// walk.
88//
89// The error return is the fix for a real inconsistency: SUM over a range
90// holding a #DIV/0! answers #DIV/0!, and SUMIF over the same range
91// answered 40 — it skipped the error along with the text and the blanks.
92// That contradicted this function's own comment, which said a
93// conditional sum must not disagree with an unconditional one about what
94// a number is. An error is not a value to skip; it is the answer.
95//
96// Neither oracle could settle it. excelize does not propagate at all —
97// it answers 40 to SUM(A1:A3) over the same range, so it disagrees with
98// our corpus-validated SUM before the conditional forms are even
99// reached — and the corpus generator refuses to record an error as a
100// value. So this follows the invariant already written here and Excel's
101// documented behaviour, and says so rather than claiming an oracle it
102// does not have.
103func total(pairs []condPair, over rect, env Env) (sum float64, n int, bad Value) {
104 at := offsetRect(over, pairs[0].over)
105 base := pairs[0].over
106
107 // The error scan is SEPARATE from the selection walk, and it has to
108 // be. An error matches no criterion — criterion.matches returns
109 // false for one, deliberately, so a bad formula does not inflate a
110 // COUNTIF — so a cell holding one is never selected and the walk
111 // below never sees it. The first version of this fix put the check
112 // inside the walk and changed nothing at all.
113 //
114 // Excel propagates an error anywhere in the sum range, matched or
115 // not, which is the same rule SUM follows over the whole range.
116 for dr := 0; dr <= base.r1-base.r0 && bad.Kind != KindErr; dr++ {
117 for dc := 0; dc <= base.c1-base.c0; dc++ {
118 if v := cellValueAt(env, at.r0+dr, at.c0+dc); v.Kind == KindErr {
119 bad = v
120 break
121 }
122 }
123 }
124 if bad.Kind == KindErr {
125 return 0, 0, bad
126 }
127
128 selected(pairs, env, func(dr, dc int) {
129 v := cellValueAt(env, at.r0+dr, at.c0+dc)
130 if v.Kind != KindNum {
131 // Text, blanks and logicals read from a reference are
132 // skipped, exactly as SUM skips them (fn_math.go's
133 // taggedValue comment).
134 return
135 }
136 sum += v.Num
137 n++
138 })
139 return sum, n, bad
140}
141
142// ---- SUMIF / COUNTIF / AVERAGEIF ------------------------------------
143//
144// The singulars take (range, criteria, [apply_range]) with the applied
145// range LAST and optional. The plurals take (apply_range, range1,
146// criteria1, …) with it FIRST and required. That inconsistency is
147// Excel's, not ours, and it is the single most common way these get
148// written wrongly — so the two shapes are decoded by separate functions
149// rather than by one with a flag.
150
151func fnSumIf(argNodes []Node, env Env) Value {
152 pairs, over, bad, ok := singular(argNodes, env)
153 if !ok {
154 return bad
155 }
156 sum, _, bad := total(pairs, over, env)
157 if bad.Kind == KindErr {
158 return bad
159 }
160 return Value{Kind: KindNum, Num: sum}
161}
162
163func fnCountIf(argNodes []Node, env Env) Value {
164 if len(argNodes) != 2 {
165 return errValue(ErrValue)
166 }
167 pairs, bad, ok := readPairs(argNodes, 0, env)
168 if !ok {
169 return bad
170 }
171 n := 0
172 selected(pairs, env, func(int, int) { n++ })
173 return Value{Kind: KindNum, Num: float64(n)}
174}
175
176func fnAverageIf(argNodes []Node, env Env) Value {
177 pairs, over, bad, ok := singular(argNodes, env)
178 if !ok {
179 return bad
180 }
181 sum, n, bad := total(pairs, over, env)
182 if bad.Kind == KindErr {
183 return bad
184 }
185 if n == 0 {
186 // Excel's answer for an average of nothing, and deliberately
187 // not zero: zero is a number somebody would act on.
188 return errValue(ErrDiv0)
189 }
190 return Value{Kind: KindNum, Num: sum / float64(n)}
191}
192
193// singular decodes the (range, criteria, [apply_range]) shape and
194// returns the rectangle to aggregate over — the criteria range itself
195// when no third argument was written, which is what makes
196// SUMIF(A1:A9,">5") sum the cells it tested.
197func singular(argNodes []Node, env Env) ([]condPair, rect, Value, bool) {
198 if len(argNodes) != 2 && len(argNodes) != 3 {
199 return nil, rect{}, errValue(ErrValue), false
200 }
201 pairs, bad, ok := readPairs(argNodes[:2], 0, env)
202 if !ok {
203 return nil, rect{}, bad, false
204 }
205 over := pairs[0].over
206 if len(argNodes) == 3 {
207 box, ok := rectOf(argNodes[2])
208 if !ok {
209 return nil, rect{}, errValue(ErrValue), false
210 }
211 over = box
212 }
213 return pairs, over, Value{}, true
214}
215
216// ---- SUMIFS / COUNTIFS / AVERAGEIFS ---------------------------------
217
218func fnSumIfs(argNodes []Node, env Env) Value {
219 over, pairs, bad, ok := plural(argNodes, env)
220 if !ok {
221 return bad
222 }
223 sum, _, bad := total(pairs, over, env)
224 if bad.Kind == KindErr {
225 return bad
226 }
227 return Value{Kind: KindNum, Num: sum}
228}
229
230func fnAverageIfs(argNodes []Node, env Env) Value {
231 over, pairs, bad, ok := plural(argNodes, env)
232 if !ok {
233 return bad
234 }
235 sum, n, bad := total(pairs, over, env)
236 if bad.Kind == KindErr {
237 return bad
238 }
239 if n == 0 {
240 return errValue(ErrDiv0)
241 }
242 return Value{Kind: KindNum, Num: sum / float64(n)}
243}
244
245// fnCountIfs has no applied range at all — it counts matches — so it
246// decodes pairs from argument zero like COUNTIF rather than through
247// plural.
248func fnCountIfs(argNodes []Node, env Env) Value {
249 pairs, bad, ok := readPairs(argNodes, 0, env)
250 if !ok {
251 return bad
252 }
253 n := 0
254 selected(pairs, env, func(int, int) { n++ })
255 return Value{Kind: KindNum, Num: float64(n)}
256}
257
258// plural decodes the (apply_range, range1, criteria1, …) shape.
259func plural(argNodes []Node, env Env) (rect, []condPair, Value, bool) {
260 if len(argNodes) < 3 {
261 return rect{}, nil, errValue(ErrValue), false
262 }
263 over, ok := rectOf(argNodes[0])
264 if !ok {
265 return rect{}, nil, errValue(ErrValue), false
266 }
267 pairs, bad, ok := readPairs(argNodes, 1, env)
268 if !ok {
269 return rect{}, nil, bad, false
270 }
271 // The SUM range's shape is checked too, and it was not.
272 // SUMIFS(A1:A2, B1:B3, "t*") answered 60 by reshaping A1:A2 up to
273 // the criteria range's height and reading A3 — a cell the formula
274 // never named. LibreOffice answers Err:502 and Excel #VALUE!.
275 // Caught by review, 2026-09-04.
276 //
277 // The singular forms are deliberately NOT checked this way: Excel's
278 // SUMIF really does read its third argument for its origin only,
279 // which is the documented asymmetry fn_cond.go's header describes.
280 if over.r1-over.r0 != pairs[0].over.r1-pairs[0].over.r0 ||
281 over.c1-over.c0 != pairs[0].over.c1-pairs[0].over.c0 {
282 return rect{}, nil, errValue(ErrValue), false
283 }
284 return over, pairs, Value{}, true
285}
286