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_lookup.go implements VLOOKUP, INDEX and MATCH. All three need a
4// range argument's shape (its width and height as written), not the
5// flattened, boundary-free []Value evalCallArgs hands an ordinary Func —
6// see registry.go's nodeFunc doc comment for why. All three register as
7// nodeFuncs and evaluate their own arguments directly against env, using
8// the same position-as-id CellKey convention refKey uses elsewhere in
9// this package (RowID = RowPos+1, ColID = ColPos+1; see eval.go).
10
11func init() {
12 registerNode("VLOOKUP", fnVLookup)
13 registerNode("INDEX", fnIndex)
14 registerNode("MATCH", fnMatch)
15}
16
17// boundedRangeBounds is rangeBounds (eval.go) plus the same cap
18// rangeCells, evalCallArg and evalAggArgs all enforce on a Range
19// (maxRangeCells; see eval.go's rangeCellCount). VLOOKUP, INDEX and MATCH
20// evaluate their range argument's shape directly against env rather than
21// through rangeCells — they need the range's four bounds to compute an
22// offset or walk cells directly, not a flattened []CellKey (see this
23// file's package doc comment) — so without this check they would be the
24// one path into a Range that never consults the cap at all: unbounded
25// synchronous iteration over a pathological reference (decodeRef accepts
26// any int row), and — because collectRefs also uses rangeCells and so
27// never wires an over-cap range's cells as precedents — a stale-cache
28// hole where these functions could read live cells the graph never
29// tracked. ok is false when the range is over the cap; callers must
30// resolve to #REF! rather than iterate, the same answer evalCallArg gives
31// an ordinary function for the same over-cap range.
32func boundedRangeBounds(rng *Range) (r0, r1, c0, c1 int, ok bool) {
33 r0, r1, c0, c1 = rangeBounds(rng)
34 _, ok = rangeCellCount(r0, r1, c0, c1)
35 return
36}
37
38func cellValueAt(env Env, row, col int) Value {
39 return env.CellValue(CellKey{RowID: uint64(row + 1), ColID: uint64(col + 1)})
40}
41
42// fnVLookup implements VLOOKUP(lookup_value, table_array, col_index_num,
43// [range_lookup]). range_lookup defaults to TRUE (approximate match,
44// requiring the first column sorted ascending — Excel's own precondition;
45// an unsorted first column gives an unspecified answer in Excel too);
46// FALSE requires an exact match. table_array must be a literal range — a
47// scalar or a call result has no shape to look up a column within.
48func fnVLookup(argNodes []Node, env Env) Value {
49 if len(argNodes) < 3 || len(argNodes) > 4 {
50 return errValue(ErrValue)
51 }
52 lookup := Eval(argNodes[0], env)
53 if lookup.Kind == KindErr {
54 return lookup
55 }
56 rng, ok := argNodes[1].(*Range)
57 if !ok {
58 return errValue(ErrValue)
59 }
60 ciV := Eval(argNodes[2], env)
61 if ciV.Kind == KindErr {
62 return ciV
63 }
64 ciF, ek := toNumber(ciV)
65 if ek != ErrNone {
66 return errValue(ek)
67 }
68 ci := int(ciF)
69
70 approx := true
71 if len(argNodes) == 4 {
72 rlV := Eval(argNodes[3], env)
73 if rlV.Kind == KindErr {
74 return rlV
75 }
76 b, ek := toBool(rlV)
77 if ek != ErrNone {
78 return errValue(ek)
79 }
80 approx = b
81 }
82
83 r0, r1, c0, c1, withinCap := boundedRangeBounds(rng)
84 if !withinCap {
85 return errValue(ErrRef)
86 }
87 cols := c1 - c0 + 1
88 if ci < 1 || ci > cols {
89 return errValue(ErrRef)
90 }
91 resultCol := c0 + ci - 1
92
93 if !approx {
94 for row := r0; row <= r1; row++ {
95 if compareValues(cellValueAt(env, row, c0), lookup) == 0 {
96 return cellValueAt(env, row, resultCol)
97 }
98 }
99 return errValue(ErrNA)
100 }
101
102 found := false
103 bestRow := 0
104 for row := r0; row <= r1; row++ {
105 if compareValues(cellValueAt(env, row, c0), lookup) <= 0 {
106 bestRow = row
107 found = true
108 continue
109 }
110 break
111 }
112 if !found {
113 return errValue(ErrNA)
114 }
115 return cellValueAt(env, bestRow, resultCol)
116}
117
118// fnIndex implements INDEX(array, row_num, [col_num]); col_num defaults
119// to 1. This engine has no array/spill Value kind, so row_num=0 or
120// col_num=0 ("return the whole row/column") is unsupported and returns
121// #VALUE! rather than silently returning one arbitrary cell of it.
122func fnIndex(argNodes []Node, env Env) Value {
123 if len(argNodes) < 2 || len(argNodes) > 3 {
124 return errValue(ErrValue)
125 }
126 rowV := Eval(argNodes[1], env)
127 if rowV.Kind == KindErr {
128 return rowV
129 }
130 rowF, ek := toNumber(rowV)
131 if ek != ErrNone {
132 return errValue(ek)
133 }
134 rowN := int(rowF)
135
136 colN := 1
137 if len(argNodes) == 3 {
138 colV := Eval(argNodes[2], env)
139 if colV.Kind == KindErr {
140 return colV
141 }
142 colF, ek := toNumber(colV)
143 if ek != ErrNone {
144 return errValue(ek)
145 }
146 colN = int(colF)
147 }
148 if rowN < 1 || colN < 1 {
149 return errValue(ErrValue)
150 }
151
152 rng, ok := argNodes[0].(*Range)
153 if !ok {
154 if rowN != 1 || colN != 1 {
155 return errValue(ErrRef)
156 }
157 return Eval(argNodes[0], env)
158 }
159 r0, r1, c0, c1, withinCap := boundedRangeBounds(rng)
160 if !withinCap {
161 return errValue(ErrRef)
162 }
163 if r0+rowN-1 > r1 || c0+colN-1 > c1 {
164 return errValue(ErrRef)
165 }
166 return cellValueAt(env, r0+rowN-1, c0+colN-1)
167}
168
169// fnMatch implements MATCH(lookup_value, lookup_array, [match_type]).
170// lookup_array must be a single row or a single column — Excel's own
171// restriction, since MATCH returns one position, not a (row, col) pair.
172// match_type: 1 (default, largest value <= lookup, ascending order),
173// 0 (exact match, any order), -1 (smallest value >= lookup, descending
174// order). No match is #N/A.
175func fnMatch(argNodes []Node, env Env) Value {
176 if len(argNodes) < 2 || len(argNodes) > 3 {
177 return errValue(ErrValue)
178 }
179 lookup := Eval(argNodes[0], env)
180 if lookup.Kind == KindErr {
181 return lookup
182 }
183 rng, ok := argNodes[1].(*Range)
184 if !ok {
185 return errValue(ErrValue)
186 }
187 matchType := 1
188 if len(argNodes) == 3 {
189 mtV := Eval(argNodes[2], env)
190 if mtV.Kind == KindErr {
191 return mtV
192 }
193 mtF, ek := toNumber(mtV)
194 if ek != ErrNone {
195 return errValue(ek)
196 }
197 matchType = int(mtF)
198 }
199
200 r0, r1, c0, c1, withinCap := boundedRangeBounds(rng)
201 if !withinCap {
202 return errValue(ErrRef)
203 }
204 if r0 != r1 && c0 != c1 {
205 return errValue(ErrNA)
206 }
207
208 switch matchType {
209 case 0:
210 return matchExact(env, r0, r1, c0, c1, lookup)
211 case 1:
212 return matchNearest(env, r0, r1, c0, c1, lookup, false)
213 case -1:
214 return matchNearest(env, r0, r1, c0, c1, lookup, true)
215 }
216 return errValue(ErrValue)
217}
218
219func matchExact(env Env, r0, r1, c0, c1 int, lookup Value) Value {
220 pos := 0
221 for row := r0; row <= r1; row++ {
222 for col := c0; col <= c1; col++ {
223 pos++
224 if compareValues(cellValueAt(env, row, col), lookup) == 0 {
225 return Value{Kind: KindNum, Num: float64(pos)}
226 }
227 }
228 }
229 return errValue(ErrNA)
230}
231
232// matchNearest implements match_type 1 (descending=false: largest value
233// <= lookup, ascending order) and match_type -1 (descending=true:
234// smallest value >= lookup, descending order). Both stop at the first
235// position that breaks the required ordering, returning the best
236// position seen so far.
237func matchNearest(env Env, r0, r1, c0, c1 int, lookup Value, descending bool) Value {
238 found := false
239 bestPos := 0
240 pos := 0
241 for row := r0; row <= r1; row++ {
242 for col := c0; col <= c1; col++ {
243 pos++
244 c := compareValues(cellValueAt(env, row, col), lookup)
245 ok := c <= 0
246 if descending {
247 ok = c >= 0
248 }
249 if ok {
250 bestPos = pos
251 found = true
252 continue
253 }
254 if found {
255 return Value{Kind: KindNum, Num: float64(bestPos)}
256 }
257 return errValue(ErrNA)
258 }
259 }
260 if found {
261 return Value{Kind: KindNum, Num: float64(bestPos)}
262 }
263 return errValue(ErrNA)
264}
265