VLOOKUP, HLOOKUP, MATCH, INDEX, XLOOKUP, CHOOSE, ROWS and COLUMNS, with sorted and exact matching as in Sheets.
- commit
- 9d6695f11c9264c53004bff8b9e48c4a6896cfef
- parent
- c3dcac1729e400af42ec4cc30af0a3c01b3240cf
- author
- Dami <dami.etoile@gmail.com>
| A | internal/ | 241 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 47 | ++++++ |
@@ -0,0 +1,241 @@
1+package sheet2+3+import (4+ "strings"5+)6+7+func init() {8+ define(9+ &FuncDef{Name: "VLOOKUP", Args: "search_key, range, index, [is_sorted]", Desc: "Find a key in the first column and return a value from its row", Min: 3, Max: 4,10+ eval: func(args []Node, get lookup) Value { return tableLookup(args, get, true) }},11+ &FuncDef{Name: "HLOOKUP", Args: "search_key, range, index, [is_sorted]", Desc: "Find a key in the first row and return a value from its column", Min: 3, Max: 4,12+ eval: func(args []Node, get lookup) Value { return tableLookup(args, get, false) }},13+ &FuncDef{Name: "MATCH", Args: "search_key, range, [search_type]", Desc: "Position of a key in a row or column", Min: 2, Max: 3,14+ eval: func(args []Node, get lookup) Value {15+ key := eval(args[0], get)16+ if key.Kind == Error {17+ return key18+ }19+ m := matrixArg(args[1], get)20+ kind, err := intArg(args, 2, 1, get)21+ switch {22+ case err != nil:23+ return *err24+ case !m.vector():25+ return ErrNA26+ }27+ var i int28+ switch {29+ case kind == 0:30+ i = findExact(key, m.size(), m.at, true)31+ case kind > 0:32+ i = findSorted(key, m.size(), m.at, 1)33+ default:34+ i = findSorted(key, m.size(), m.at, -1)35+ }36+ if i < 0 {37+ return ErrNA38+ }39+ return num(float64(i + 1))40+ }},41+ &FuncDef{Name: "INDEX", Args: "reference, [row], [column]", Desc: "The value at a row and column of a range", Min: 1, Max: 3,42+ eval: func(args []Node, get lookup) Value {43+ m := matrixArg(args[0], get)44+ row, err := intArg(args, 1, 0, get)45+ if err != nil {46+ return *err47+ }48+ col, err := intArg(args, 2, 0, get)49+ if err != nil {50+ return *err51+ }52+ // In a single row, a lone index counts along the row.53+ if m.rows == 1 && len(args) == 2 {54+ row, col = 1, row55+ }56+ if row < 0 || col < 0 {57+ return ErrValue58+ }59+ if row == 0 && m.rows == 1 {60+ row = 161+ }62+ if col == 0 && m.cols == 1 {63+ col = 164+ }65+ switch {66+ case row == 0 || col == 0:67+ return ErrValue // a whole row or column: an array we can't show68+ case row > m.rows || col > m.cols:69+ return ErrRef70+ }71+ return m.cell(row-1, col-1)72+ }, format: inheritFrom(0)},73+ &FuncDef{Name: "XLOOKUP", Args: "search_key, lookup_range, result_range, [missing_value], [match_mode], [search_mode]", Desc: "Find a key and return the matching entry of another range", Min: 3, Max: 6,74+ eval: xlookup, format: inheritFrom(2)},75+ &FuncDef{Name: "CHOOSE", Args: "index, choice1, [choice2, ...]", Desc: "The choice at a position", Min: 2, Max: -1,76+ eval: func(args []Node, get lookup) Value {77+ i, err := intArg(args, 0, 0, get)78+ if err != nil {79+ return *err80+ }81+ if i < 1 || i >= len(args) {82+ return ErrValue83+ }84+ return eval(args[i], get)85+ }},86+ &FuncDef{Name: "ROWS", Args: "range", Desc: "Number of rows in a range", Min: 1, Max: 1,87+ eval: func(args []Node, get lookup) Value { return num(float64(matrixArg(args[0], get).rows)) }},88+ &FuncDef{Name: "COLUMNS", Args: "range", Desc: "Number of columns in a range", Min: 1, Max: 1,89+ eval: func(args []Node, get lookup) Value { return num(float64(matrixArg(args[0], get).cols)) }},90+ )91+}92+93+// tableLookup is VLOOKUP (vertical) and HLOOKUP: search the first column94+// (or row), then return the index-th entry across.95+func tableLookup(args []Node, get lookup, vertical bool) Value {96+ key := eval(args[0], get)97+ if key.Kind == Error {98+ return key99+ }100+ m := matrixArg(args[1], get)101+ idx, err := intArg(args, 2, 0, get)102+ if err != nil {103+ return *err104+ }105+ sorted, err := boolArg(args, 3, true, get)106+ if err != nil {107+ return *err108+ }109+ n, across := m.rows, m.cols110+ at := func(i int) Value { return m.cell(i, 0) }111+ if !vertical {112+ n, across = m.cols, m.rows113+ at = func(i int) Value { return m.cell(0, i) }114+ }115+ switch {116+ case idx < 1:117+ return ErrValue118+ case idx > across:119+ return ErrRef120+ }121+ var i int122+ if sorted {123+ i = findSorted(key, n, at, 1)124+ } else {125+ i = findExact(key, n, at, true)126+ }127+ if i < 0 {128+ return ErrNA129+ }130+ if vertical {131+ return m.cell(i, idx-1)132+ }133+ return m.cell(idx-1, i)134+}135+136+// lookupEqual reports whether a lookup key matches v: same kind, text137+// case-insensitive, with wildcards when allowed.138+func lookupEqual(key, v Value, wild bool) bool {139+ if key.Kind != v.Kind {140+ return false141+ }142+ if key.Kind == Text {143+ if wild && hasWildcards(key.Str) {144+ return wildMatch(key.Str, v.Str)145+ }146+ return strings.EqualFold(key.Str, v.Str)147+ }148+ return key.Num == v.Num149+}150+151+// findExact returns the first index whose value equals key, or -1.152+func findExact(key Value, n int, at func(int) Value, wild bool) int {153+ for i := range n {154+ if lookupEqual(key, at(i), wild) {155+ return i156+ }157+ }158+ return -1159+}160+161+// findSorted is the approximate match of sorted lookups: with dir 1 the162+// last value <= key in ascending data, with dir -1 the last value >= key163+// in descending data. Values of another kind are skipped.164+func findSorted(key Value, n int, at func(int) Value, dir int) int {165+ found := -1166+ for i := range n {167+ v := at(i)168+ if v.Kind != key.Kind {169+ continue170+ }171+ if c := compare(v, key) * dir; c > 0 {172+ break173+ }174+ found = i175+ }176+ return found177+}178+179+func xlookup(args []Node, get lookup) Value {180+ key := eval(args[0], get)181+ if key.Kind == Error {182+ return key183+ }184+ look := matrixArg(args[1], get)185+ res := matrixArg(args[2], get)186+ mode, err := intArg(args, 4, 0, get)187+ if err != nil {188+ return *err189+ }190+ search, err := intArg(args, 5, 1, get)191+ if err != nil {192+ return *err193+ }194+ switch {195+ case !look.vector():196+ return ErrValue197+ case mode < -1 || mode > 2 || search == 0 || search < -2 || search > 2:198+ return ErrValue199+ }200+ // The result range lines up with the lookup range along its length.201+ n := look.size()202+ var pick func(i int) Value203+ switch {204+ case look.cols == 1 && res.rows == n:205+ pick = func(i int) Value { return res.cell(i, 0) }206+ case look.rows == 1 && res.cols == n:207+ pick = func(i int) Value { return res.cell(0, i) }208+ default:209+ return ErrValue210+ }211+ order := make([]int, n)212+ for i := range order {213+ order[i] = i214+ if search < 0 {215+ order[i] = n - 1 - i216+ }217+ }218+ best := -1219+ for _, i := range order {220+ v := look.at(i)221+ if lookupEqual(key, v, mode == 2) {222+ best = i223+ break224+ }225+ if mode == 0 || mode == 2 || v.Kind != key.Kind {226+ continue227+ }228+ // Next smaller (-1) or next larger (1): keep the closest.229+ c := compare(v, key)230+ if c*mode > 0 && (best < 0 || compare(v, look.at(best))*mode < 0) {231+ best = i232+ }233+ }234+ if best < 0 {235+ if given(args, 3) {236+ return eval(args[3], get)237+ }238+ return ErrNA239+ }240+ return pick(best)241+}@@ -0,0 +1,47 @@
1+package sheet2+3+import "testing"4+5+func TestLookupFunctions(t *testing.T) {6+ checkFormulas(t, fixture(t), []fnCase{7+ {`=VLOOKUP("banana", D1:E3, 2, FALSE)`, num(2.5)},8+ {`=VLOOKUP("B*", D1:E3, 2, FALSE)`, num(2.5)},9+ {`=VLOOKUP("kiwi", D1:E3, 2, FALSE)`, ErrNA},10+ {`=VLOOKUP("Apple", D1:E5, 2, FALSE)`, num(1.5)}, // first match11+ {`=VLOOKUP("Blueberry", D1:E3, 2)`, num(2.5)}, // sorted: largest <= key12+ {`=VLOOKUP("Aardvark", D1:E3, 2, TRUE)`, ErrNA},13+ {`=VLOOKUP(2.5, C1:D5, 2)`, txt("Banana")},14+ {`=VLOOKUP("Apple", D1:E3, 3, FALSE)`, ErrRef},15+ {`=VLOOKUP("Apple", D1:E3, 0, FALSE)`, ErrValue},16+ {`=HLOOKUP(1.5, C1:E2, 2, FALSE)`, num(2.5)},17+ {`=HLOOKUP("Apple", D1:E2, 2, FALSE)`, txt("Banana")},18+ {`=MATCH("Cherry", D1:D3, 0)`, num(3)},19+ {`=MATCH(3.2, C1:C7)`, num(3)},20+ {`=MATCH(3.2, C1:C7, 1)`, num(3)},21+ {`=MATCH(0, C1:C7)`, ErrNA},22+ {`=MATCH(25, I1:I3, -1)`, num(1)}, // descending: smallest >= key23+ {`=MATCH(10, I1:I3, -1)`, num(3)},24+ {`=MATCH(35, I1:I3, -1)`, ErrNA},25+ {`=MATCH(2, C1:D2, 0)`, ErrNA}, // not a single row or column26+ {"=INDEX(D1:E3, 2, 2)", num(2.5)},27+ {"=INDEX(D1:D3, 3)", txt("Cherry")},28+ {"=INDEX(C1:E1, 2)", txt("Apple")}, // a single row counts across29+ {"=INDEX(D1:E3, 4, 1)", ErrRef},30+ {"=INDEX(D1:E3, 2)", ErrValue}, // a whole row31+ {`=XLOOKUP("Cherry", D1:D3, E1:E3)`, num(3.5)},32+ {`=XLOOKUP("kiwi", D1:D3, E1:E3)`, ErrNA},33+ {`=XLOOKUP("kiwi", D1:D3, E1:E3, "none")`, txt("none")},34+ {`=XLOOKUP("Apple", D1:D5, E1:E5, , 0, -1)`, num(9)}, // last to first35+ {`=XLOOKUP(3.2, C1:C7, D1:D7, , -1)`, txt("Cherry")}, // next smaller36+ {`=XLOOKUP(3.2, C1:C7, D1:D7, , 1)`, txt("date")}, // next larger37+ {`=XLOOKUP("Ch*", D1:D3, E1:E3, , 2)`, num(3.5)},38+ {`=XLOOKUP("Apple", C1:E1, C2:E2)`, txt("Banana")}, // across a row39+ {`=XLOOKUP(1, C1:C3, E1:E2)`, ErrValue},40+ {`=CHOOSE(2, "a", "b", "c")`, txt("b")},41+ {`=CHOOSE(2.9, "a", "b", "c")`, txt("b")},42+ {`=CHOOSE(4, "a", "b", "c")`, ErrValue},43+ {`=CHOOSE(1, "a", 1/0)`, txt("a")}, // only the chosen value is evaluated44+ {"=ROWS(A1:C7)", num(7)},45+ {"=COLUMNS(A1:C7)", num(3)},46+ })47+}