cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Add lookup functions

Dami committed 9d6695f
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>
2 files changed, +288 -0
Ainternal/sheet/functions_lookup.go241++++++++++++++++++++++++++++++
Ainternal/sheet/functions_lookup_test.go47++++++
A internal/sheet/functions_lookup.go +241 -0
@@ -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+}
A internal/sheet/functions_lookup_test.go +47 -0
@@ -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+}