cat / 012
clone

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

Name the evaluators in the function tables

Dami committed a7278b9
The multi-line evaluators written inline in the text, statistics,
lookup, date and logic tables become named functions after each table,
so the tables read as one entry per function and their init functions
drop from cognitive complexity 26-62 to a few. Behavior and the
generated docs/functions.md are unchanged.
commit
a7278b9d023217607598e946d7ac99fc55cc6ef1
parent
e1e9e30b972d7f2ba5c904119dd664e487154d06
author
Dami <dami.etoile@gmail.com>
5 files changed, +496 -428
Minternal/sheet/functions_date.go162++++++++-------
Minternal/sheet/functions_logic.go100+++++----
Minternal/sheet/functions_lookup.go144+++++++------
Minternal/sheet/functions_stats.go202++++++++++---------
Minternal/sheet/functions_text.go316++++++++++++++++--------------
M internal/sheet/functions_date.go +88 -74
@@ -16,28 +16,9 @@ var (
1616 func init() {1717 define(1818 &FuncDef{Name: "DATE", Args: "year, month, day", Desc: "A date from its parts; months and days past the end roll over", Min: 3, Max: 3,19- eval: numeric(func(x []float64) Value {20- y, m, d := math.Trunc(x[0]), math.Trunc(x[1]), math.Trunc(x[2])21- if y >= 0 && y < 1900 {22- y += 1900 // DATE(26, 1, 1) is 1926, as in Sheets23- }24- if y < 0 || y >= 10000 || math.Abs(m) > 1e6 || math.Abs(d) > 1e8 {25- return ErrNum26- }27- v := numfmt.DateSerial(int(y), int(m), int(d))28- if v < 0 {29- return ErrNum30- }31- return num(v)32- }), format: dateFormat},19+ eval: numeric(date), format: dateFormat},3320 &FuncDef{Name: "TIME", Args: "hour, minute, second", Desc: "A time of day from its parts", Min: 3, Max: 3,34- eval: numeric(func(x []float64) Value {35- secs := math.Trunc(x[0])*3600 + math.Trunc(x[1])*60 + math.Trunc(x[2])36- if secs < 0 {37- return ErrNum38- }39- return num(math.Mod(secs, 86400) / 86400)40- }), format: timeFormat},21+ eval: numeric(timeOfDay), format: timeFormat},4122 &FuncDef{Name: "TODAY", Desc: "Today's date, updated on every change", Max: 0, Volatile: true,4223 eval: func([]Node, lookup) Value { return num(math.Floor(numfmt.SerialOf(Now()))) }, format: dateFormat},4324 &FuncDef{Name: "NOW", Desc: "The current date and time, updated on every change", Max: 0, Volatile: true,
@@ -49,27 +30,7 @@ func init() {
4930 &FuncDef{Name: "DAY", Args: "date", Desc: "Day of the month of a date", Min: 1, Max: 1,5031 eval: datePart(func(_, _, d int) int { return d })},5132 &FuncDef{Name: "WEEKDAY", Args: "date, [type]", Desc: "Day of the week: 1 is Sunday, or Monday with type 2", Min: 1, Max: 2,52- eval: func(args []Node, get lookup) Value {53- d, err := dateArg(args[0], get)54- if err != nil {55- return *err56- }57- typ, err := intArg(args, 1, 1, get)58- if err != nil {59- return *err60- }61- days, _ := numfmt.SplitSerial(d)62- wd := numfmt.Weekday(days) // 0 = Sunday63- switch typ {64- case 1:65- return num(float64(wd + 1))66- case 2:67- return num(float64((wd+6)%7 + 1))68- case 3:69- return num(float64((wd + 6) % 7))70- }71- return ErrNum72- }},33+ eval: weekday},7334 &FuncDef{Name: "HOUR", Args: "time", Desc: "Hour of a time, 0 to 23", Min: 1, Max: 1,7435 eval: timePart(func(s int) int { return s / 3600 })},7536 &FuncDef{Name: "MINUTE", Args: "time", Desc: "Minute of a time, 0 to 59", Min: 1, Max: 1,
@@ -83,45 +44,98 @@ func init() {
8344 &FuncDef{Name: "DATEDIF", Args: `start_date, end_date, unit`, Desc: `Time between dates in "Y", "M", "D", "MD", "YM" or "YD"`, Min: 3, Max: 3,8445 eval: datedif},8546 &FuncDef{Name: "DAYS", Args: "end_date, start_date", Desc: "Number of days between two dates", Min: 2, Max: 2,86- eval: func(args []Node, get lookup) Value {87- end, err := dateArg(args[0], get)88- if err != nil {89- return *err90- }91- start, err := dateArg(args[1], get)92- if err != nil {93- return *err94- }95- return num(math.Floor(end) - math.Floor(start))96- }},47+ eval: days},9748 &FuncDef{Name: "NETWORKDAYS", Args: "start_date, end_date, [holidays]", Desc: "Number of weekdays between two dates, counting both", Min: 2, Max: 3,9849 eval: networkdays},9950 &FuncDef{Name: "DATEVALUE", Args: "date_string", Desc: "The date a text such as \"2026-09-26\" stands for", Min: 1, Max: 1,100- eval: func(args []Node, get lookup) Value {101- v, ok := parsedText(args[0], get)102- if ok != nil {103- return *ok104- }105- d, f, parsed := parseDateTime(v)106- if !parsed || f.Kind == FmtTime || f.Kind == FmtDuration {107- return ErrValue108- }109- return num(math.Floor(d))110- }, format: dateFormat},51+ eval: dateValue, format: dateFormat},11152 &FuncDef{Name: "TIMEVALUE", Args: "time_string", Desc: "The time of day a text such as \"2:30 PM\" stands for", Min: 1, Max: 1,112- eval: func(args []Node, get lookup) Value {113- v, ok := parsedText(args[0], get)114- if ok != nil {115- return *ok116- }117- if d, _, parsed := parseDateTime(v); parsed {118- return num(d - math.Floor(d))119- }120- return ErrValue121- }, format: timeFormat},53+ eval: timeValue, format: timeFormat},12254 )12355 }12456 57+// Evaluators for the table above, in its order.58+59+func date(x []float64) Value {60+ y, m, d := math.Trunc(x[0]), math.Trunc(x[1]), math.Trunc(x[2])61+ if y >= 0 && y < 1900 {62+ y += 1900 // DATE(26, 1, 1) is 1926, as in Sheets63+ }64+ if y < 0 || y >= 10000 || math.Abs(m) > 1e6 || math.Abs(d) > 1e8 {65+ return ErrNum66+ }67+ v := numfmt.DateSerial(int(y), int(m), int(d))68+ if v < 0 {69+ return ErrNum70+ }71+ return num(v)72+}73+74+func timeOfDay(x []float64) Value {75+ secs := math.Trunc(x[0])*3600 + math.Trunc(x[1])*60 + math.Trunc(x[2])76+ if secs < 0 {77+ return ErrNum78+ }79+ return num(math.Mod(secs, 86400) / 86400)80+}81+82+func weekday(args []Node, get lookup) Value {83+ d, err := dateArg(args[0], get)84+ if err != nil {85+ return *err86+ }87+ typ, err := intArg(args, 1, 1, get)88+ if err != nil {89+ return *err90+ }91+ days, _ := numfmt.SplitSerial(d)92+ wd := numfmt.Weekday(days) // 0 = Sunday93+ switch typ {94+ case 1:95+ return num(float64(wd + 1))96+ case 2:97+ return num(float64((wd+6)%7 + 1))98+ case 3:99+ return num(float64((wd + 6) % 7))100+ }101+ return ErrNum102+}103+104+func days(args []Node, get lookup) Value {105+ end, err := dateArg(args[0], get)106+ if err != nil {107+ return *err108+ }109+ start, err := dateArg(args[1], get)110+ if err != nil {111+ return *err112+ }113+ return num(math.Floor(end) - math.Floor(start))114+}115+116+func dateValue(args []Node, get lookup) Value {117+ v, ok := parsedText(args[0], get)118+ if ok != nil {119+ return *ok120+ }121+ d, f, parsed := parseDateTime(v)122+ if !parsed || f.Kind == FmtTime || f.Kind == FmtDuration {123+ return ErrValue124+ }125+ return num(math.Floor(d))126+}127+128+func timeValue(args []Node, get lookup) Value {129+ v, ok := parsedText(args[0], get)130+ if ok != nil {131+ return *ok132+ }133+ if d, _, parsed := parseDateTime(v); parsed {134+ return num(d - math.Floor(d))135+ }136+ return ErrValue137+}138+125139 // parsedText requires a text argument, as DATEVALUE and TIMEVALUE do.126140 func parsedText(n Node, get lookup) (string, *Value) {127141 v := eval(n, get)
M internal/sheet/functions_logic.go +55 -45
@@ -3,55 +3,13 @@ package sheet
33 func init() {44 define(55 &FuncDef{Name: "IFS", Args: "condition1, value1, [condition2, value2, ...]", Desc: "The value of the first true condition", Min: 2, Max: -1, step: 2,6- eval: func(args []Node, get lookup) Value {7- for i := 0; i+1 < len(args); i += 2 {8- c, err := numArg(args[i], get)9- if err != nil {10- return *err11- }12- if c != 0 {13- return eval(args[i+1], get)14- }15- }16- return ErrNA17- }, format: func(args []Node, infer func(Node) Format) Format {18- var vals []Node19- for i := 1; i < len(args); i += 2 {20- vals = append(vals, args[i])21- }22- return inherit(vals, infer)23- }},6+ eval: ifs, format: ifsFormat},247 &FuncDef{Name: "SWITCH", Args: "expression, case1, value1, [case2, value2, ...], [default]", Desc: "The value for the first case equal to an expression", Min: 3, Max: -1,25- eval: func(args []Node, get lookup) Value {26- x := eval(args[0], get)27- if x.Kind == Error {28- return x29- }30- i := 131- for ; i+1 < len(args); i += 2 {32- c := eval(args[i], get)33- if c.Kind == Error {34- return c35- }36- if sameKind(x, c) && compare(x, c) == 0 {37- return eval(args[i+1], get)38- }39- }40- if i < len(args) {41- return eval(args[i], get)42- }43- return ErrNA44- }},8+ eval: switchCase},459 &FuncDef{Name: "XOR", Args: "logical1, [logical2, ...]", Desc: "TRUE if an odd number of arguments are true", Min: 1, Max: -1,4610 eval: logical(func(t, _ int) bool { return t%2 == 1 })},4711 &FuncDef{Name: "IFNA", Args: "value, value_if_na", Desc: "A fallback when a value is #N/A", Min: 2, Max: 2,48- eval: func(args []Node, get lookup) Value {49- v := eval(args[0], get)50- if v == ErrNA {51- return eval(args[1], get)52- }53- return v54- }, format: inherit},12+ eval: ifNA, format: inherit},5513 &FuncDef{Name: "ISBLANK", Args: "value", Desc: "TRUE if a cell is empty", Min: 1, Max: 1,5614 eval: is(func(v Value) bool { return v.Kind == Empty })},5715 &FuncDef{Name: "ISNUMBER", Args: "value", Desc: "TRUE if a value is a number", Min: 1, Max: 1,
@@ -69,6 +27,58 @@ func init() {
6927 )7028 }7129 30+// Evaluators for the table above, in its order.31+32+func ifs(args []Node, get lookup) Value {33+ for i := 0; i+1 < len(args); i += 2 {34+ c, err := numArg(args[i], get)35+ if err != nil {36+ return *err37+ }38+ if c != 0 {39+ return eval(args[i+1], get)40+ }41+ }42+ return ErrNA43+}44+45+func ifsFormat(args []Node, infer func(Node) Format) Format {46+ var vals []Node47+ for i := 1; i < len(args); i += 2 {48+ vals = append(vals, args[i])49+ }50+ return inherit(vals, infer)51+}52+53+func switchCase(args []Node, get lookup) Value {54+ x := eval(args[0], get)55+ if x.Kind == Error {56+ return x57+ }58+ i := 159+ for ; i+1 < len(args); i += 2 {60+ c := eval(args[i], get)61+ if c.Kind == Error {62+ return c63+ }64+ if sameKind(x, c) && compare(x, c) == 0 {65+ return eval(args[i+1], get)66+ }67+ }68+ if i < len(args) {69+ return eval(args[i], get)70+ }71+ return ErrNA72+}73+74+func ifNA(args []Node, get lookup) Value {75+ v := eval(args[0], get)76+ if v == ErrNA {77+ return eval(args[1], get)78+ }79+ return v80+}81+7282 // is builds the IS functions, which never return an error themselves.7383 func is(test func(Value) bool) func([]Node, lookup) Value {7484 return func(args []Node, get lookup) Value {
M internal/sheet/functions_lookup.go +76 -68
@@ -11,78 +11,13 @@ func init() {
1111 &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,1212 eval: func(args []Node, get lookup) Value { return tableLookup(args, get, false) }},1313 &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- }},14+ eval: match},4115 &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)},16+ eval: index, format: inheritFrom(0)},7317 &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,7418 eval: xlookup, format: inheritFrom(2)},7519 &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- }},20+ eval: choose},8621 &FuncDef{Name: "ROWS", Args: "range", Desc: "Number of rows in a range", Min: 1, Max: 1,8722 eval: func(args []Node, get lookup) Value { return num(float64(matrixArg(args[0], get).rows)) }},8823 &FuncDef{Name: "COLUMNS", Args: "range", Desc: "Number of columns in a range", Min: 1, Max: 1,
@@ -90,6 +25,79 @@ func init() {
9025 )9126 }9227 28+// Evaluators for the table above, in its order.29+30+func match(args []Node, get lookup) Value {31+ key := eval(args[0], get)32+ if key.Kind == Error {33+ return key34+ }35+ m := matrixArg(args[1], get)36+ kind, err := intArg(args, 2, 1, get)37+ switch {38+ case err != nil:39+ return *err40+ case !m.vector():41+ return ErrNA42+ }43+ var i int44+ switch {45+ case kind == 0:46+ i = findExact(key, m.size(), m.at, true)47+ case kind > 0:48+ i = findSorted(key, m.size(), m.at, 1)49+ default:50+ i = findSorted(key, m.size(), m.at, -1)51+ }52+ if i < 0 {53+ return ErrNA54+ }55+ return num(float64(i + 1))56+}57+58+func index(args []Node, get lookup) Value {59+ m := matrixArg(args[0], get)60+ row, err := intArg(args, 1, 0, get)61+ if err != nil {62+ return *err63+ }64+ col, err := intArg(args, 2, 0, get)65+ if err != nil {66+ return *err67+ }68+ // In a single row, a lone index counts along the row.69+ if m.rows == 1 && len(args) == 2 {70+ row, col = 1, row71+ }72+ if row < 0 || col < 0 {73+ return ErrValue74+ }75+ if row == 0 && m.rows == 1 {76+ row = 177+ }78+ if col == 0 && m.cols == 1 {79+ col = 180+ }81+ switch {82+ case row == 0 || col == 0:83+ return ErrValue // a whole row or column: an array we can't show84+ case row > m.rows || col > m.cols:85+ return ErrRef86+ }87+ return m.cell(row-1, col-1)88+}89+90+func choose(args []Node, get lookup) Value {91+ i, err := intArg(args, 0, 0, get)92+ if err != nil {93+ return *err94+ }95+ if i < 1 || i >= len(args) {96+ return ErrValue97+ }98+ return eval(args[i], get)99+}100+93101 // tableLookup is VLOOKUP (vertical) and HLOOKUP: search the first column94102 // (or row), then return the index-th entry across.95103 func tableLookup(args []Node, get lookup, vertical bool) Value {
M internal/sheet/functions_stats.go +108 -94
@@ -8,81 +8,19 @@ import (
88 func init() {99 define(1010 &FuncDef{Name: "AVERAGEIF", Args: "criteria_range, criterion, [average_range]", Desc: "Average of the cells that meet a condition", Min: 2, Max: 3,11- eval: func(args []Node, get lookup) Value {12- rng := matrixArg(args[0], get)13- cv := eval(args[1], get)14- if cv.Kind == Error {15- return cv16- }17- c := newCriterion(cv)18- avg := rng19- if len(args) > 2 {20- avg = matrixArg(args[2], get).resized(rng.rows, rng.cols, get)21- }22- mask := make([]bool, rng.size())23- for i := range mask {24- mask[i] = c.test(rng.at(i))25- }26- return averageMasked(avg, mask)27- }, format: inheritFrom(2)},11+ eval: averageIf, format: inheritFrom(2)},2812 &FuncDef{Name: "AVERAGEIFS", Args: "average_range, criteria_range1, criterion1, [criteria_range2, criterion2, ...]", Desc: "Average of the cells that meet every condition", Min: 3, Max: -1, step: 2,29- eval: func(args []Node, get lookup) Value {30- avg := matrixArg(args[0], get)31- rows, cols, mask, err := criteriaMask(args, 1, get)32- switch {33- case err != nil:34- return *err35- case rows != avg.rows || cols != avg.cols:36- return ErrValue37- }38- return averageMasked(avg, mask)39- }, format: inheritFrom(0)},13+ eval: averageIfs, format: inheritFrom(0)},4014 &FuncDef{Name: "COUNTIF", Args: "range, criterion", Desc: "Count of the cells that meet a condition", Min: 2, Max: 2,4115 eval: countIfs},4216 &FuncDef{Name: "COUNTIFS", Args: "criteria_range1, criterion1, [criteria_range2, criterion2, ...]", Desc: "Count of the cells that meet every condition", Min: 2, Max: -1, step: 2,4317 eval: countIfs},4418 &FuncDef{Name: "COUNTBLANK", Args: "range", Desc: "Count of empty cells, including empty text", Min: 1, Max: 1,45- eval: func(args []Node, get lookup) Value {46- m := matrixArg(args[0], get)47- n := 048- for i := range m.size() {49- if v := m.at(i); v.Kind == Empty || v.Kind == Text && v.Str == "" {50- n++51- }52- }53- return num(float64(n))54- }},19+ eval: countBlank},5520 &FuncDef{Name: "MEDIAN", Args: "value1, [value2, ...]", Desc: "Middle value of numbers", Min: 1, Max: -1,56- eval: withNums(func(x []float64) Value {57- if len(x) == 0 {58- return ErrNum59- }60- slices.Sort(x)61- n := len(x)62- if n%2 == 1 {63- return num(x[n/2])64- }65- return num((x[n/2-1] + x[n/2]) / 2)66- }), format: inherit},21+ eval: withNums(median), format: inherit},6722 &FuncDef{Name: "MODE", Args: "value1, [value2, ...]", Desc: "Most common number (the first, on a tie)", Min: 1, Max: -1,68- eval: withNums(func(x []float64) Value {69- best, bestN := 0.0, 170- for i, v := range x {71- n := 072- for _, w := range x[i:] {73- if w == v {74- n++75- }76- }77- if n > bestN {78- best, bestN = v, n79- }80- }81- if bestN < 2 {82- return ErrNA83- }84- return num(best)85- }), format: inherit},23+ eval: withNums(mode), format: inherit},8624 &FuncDef{Name: "STDEV", Args: "value1, [value2, ...]", Desc: "Standard deviation of a sample", Min: 1, Max: -1,8725 eval: withNums(func(x []float64) Value { return spread(x, 1, true) })},8826 &FuncDef{Name: "STDEVP", Args: "value1, [value2, ...]", Desc: "Standard deviation of a whole population", Min: 1, Max: -1,
@@ -96,36 +34,112 @@ func init() {
9634 &FuncDef{Name: "SMALL", Args: "data, n", Desc: "The nth smallest number", Min: 2, Max: 2,9735 eval: nth(cmpFloat), format: inheritFrom(0)},9836 &FuncDef{Name: "RANK", Args: "value, data, [is_ascending]", Desc: "Rank of a number among others, largest first by default", Min: 2, Max: 3,99- eval: func(args []Node, get lookup) Value {100- v, err := numArg(args[0], get)101- if err != nil {102- return *err103- }104- data, err := nums(args[1:2], get)105- if err != nil {106- return *err107- }108- asc, err := boolArg(args, 2, false, get)109- if err != nil {110- return *err111- }112- rank, found := 1, false113- for _, d := range data {114- switch {115- case d == v:116- found = true117- case asc && d < v, !asc && d > v:118- rank++119- }120- }121- if !found {122- return ErrNA123- }124- return num(float64(rank))125- }},37+ eval: rank},12638 )12739 }12840 41+// Evaluators for the table above, in its order.42+43+func averageIf(args []Node, get lookup) Value {44+ rng := matrixArg(args[0], get)45+ cv := eval(args[1], get)46+ if cv.Kind == Error {47+ return cv48+ }49+ c := newCriterion(cv)50+ avg := rng51+ if len(args) > 2 {52+ avg = matrixArg(args[2], get).resized(rng.rows, rng.cols, get)53+ }54+ mask := make([]bool, rng.size())55+ for i := range mask {56+ mask[i] = c.test(rng.at(i))57+ }58+ return averageMasked(avg, mask)59+}60+61+func averageIfs(args []Node, get lookup) Value {62+ avg := matrixArg(args[0], get)63+ rows, cols, mask, err := criteriaMask(args, 1, get)64+ switch {65+ case err != nil:66+ return *err67+ case rows != avg.rows || cols != avg.cols:68+ return ErrValue69+ }70+ return averageMasked(avg, mask)71+}72+73+func countBlank(args []Node, get lookup) Value {74+ m := matrixArg(args[0], get)75+ n := 076+ for i := range m.size() {77+ if v := m.at(i); v.Kind == Empty || v.Kind == Text && v.Str == "" {78+ n++79+ }80+ }81+ return num(float64(n))82+}83+84+func median(x []float64) Value {85+ if len(x) == 0 {86+ return ErrNum87+ }88+ slices.Sort(x)89+ n := len(x)90+ if n%2 == 1 {91+ return num(x[n/2])92+ }93+ return num((x[n/2-1] + x[n/2]) / 2)94+}95+96+func mode(x []float64) Value {97+ best, bestN := 0.0, 198+ for i, v := range x {99+ n := 0100+ for _, w := range x[i:] {101+ if w == v {102+ n++103+ }104+ }105+ if n > bestN {106+ best, bestN = v, n107+ }108+ }109+ if bestN < 2 {110+ return ErrNA111+ }112+ return num(best)113+}114+115+func rank(args []Node, get lookup) Value {116+ v, err := numArg(args[0], get)117+ if err != nil {118+ return *err119+ }120+ data, err := nums(args[1:2], get)121+ if err != nil {122+ return *err123+ }124+ asc, err := boolArg(args, 2, false, get)125+ if err != nil {126+ return *err127+ }128+ rank, found := 1, false129+ for _, d := range data {130+ switch {131+ case d == v:132+ found = true133+ case asc && d < v, !asc && d > v:134+ rank++135+ }136+ }137+ if !found {138+ return ErrNA139+ }140+ return num(float64(rank))141+}142+129143 func countIfs(args []Node, get lookup) Value {130144 _, _, mask, err := criteriaMask(args, 0, get)131145 if err != nil {
M internal/sheet/functions_text.go +169 -147
@@ -11,71 +11,17 @@ const maxText = 50000
1111 func init() {1212 define(1313 &FuncDef{Name: "CONCATENATE", Args: "string1, [string2, ...]", Desc: "Join text, including every cell of ranges", Min: 1, Max: -1,14- eval: func(args []Node, get lookup) Value {15- parts, err := texts(args, get)16- if err != nil {17- return *err18- }19- return str(strings.Join(parts, ""))20- }},14+ eval: concatenate},2115 &FuncDef{Name: "CONCAT", Args: "value1, value2", Desc: "Join two values as text", Min: 2, Max: 2,22- eval: func(args []Node, get lookup) Value {23- a, err := textArg(args[0], get)24- if err != nil {25- return *err26- }27- b, err := textArg(args[1], get)28- if err != nil {29- return *err30- }31- return str(a + b)32- }},16+ eval: concat},3317 &FuncDef{Name: "TEXTJOIN", Args: "delimiter, ignore_empty, text1, [text2, ...]", Desc: "Join text with a delimiter", Min: 3, Max: -1,34- eval: func(args []Node, get lookup) Value {35- delim, err := textArg(args[0], get)36- if err != nil {37- return *err38- }39- skip, err := boolArg(args, 1, true, get)40- if err != nil {41- return *err42- }43- parts, err := texts(args[2:], get)44- if err != nil {45- return *err46- }47- if skip {48- parts = slicesDeleteEmpty(parts)49- }50- return capText(strings.Join(parts, delim))51- }},18+ eval: textJoin},5219 &FuncDef{Name: "LEFT", Args: "string, [number_of_characters]", Desc: "The first characters of text", Min: 1, Max: 2,5320 eval: textSlice(func(r []rune, n int) string { return string(r[:min(n, len(r))]) })},5421 &FuncDef{Name: "RIGHT", Args: "string, [number_of_characters]", Desc: "The last characters of text", Min: 1, Max: 2,5522 eval: textSlice(func(r []rune, n int) string { return string(r[len(r)-min(n, len(r)):]) })},5623 &FuncDef{Name: "MID", Args: "string, starting_at, extract_length", Desc: "Characters from the middle of text", Min: 3, Max: 3,57- eval: func(args []Node, get lookup) Value {58- s, err := textArg(args[0], get)59- if err != nil {60- return *err61- }62- start, err := intArg(args, 1, 1, get)63- if err != nil {64- return *err65- }66- n, err := intArg(args, 2, 0, get)67- if err != nil {68- return *err69- }70- if start < 1 || n < 0 {71- return ErrValue72- }73- r := []rune(s)74- if start > len(r) {75- return str("")76- }77- return str(string(r[start-1 : min(start-1+n, len(r))]))78- }},24+ eval: mid},7925 &FuncDef{Name: "LEN", Args: "text", Desc: "Number of characters in text", Min: 1, Max: 1,8026 eval: textFn(func(s string) Value { return num(float64(runeLen(s))) })},8127 &FuncDef{Name: "UPPER", Args: "text", Desc: "Text in upper case", Min: 1, Max: 1,
@@ -85,109 +31,185 @@ func init() {
8531 &FuncDef{Name: "PROPER", Args: "text", Desc: "Text with each word capitalized", Min: 1, Max: 1,8632 eval: textFn(func(s string) Value { return str(proper(s)) })},8733 &FuncDef{Name: "TRIM", Args: "text", Desc: "Text without leading, trailing and repeated spaces", Min: 1, Max: 1,88- eval: textFn(func(s string) Value {89- return str(strings.Join(strings.FieldsFunc(s, func(r rune) bool { return r == ' ' }), " "))90- })},34+ eval: textFn(trimSpaces)},9135 &FuncDef{Name: "SUBSTITUTE", Args: "text, search_for, replace_with, [occurrence_number]", Desc: "Replace occurrences of text", Min: 3, Max: 4,9236 eval: substitute},9337 &FuncDef{Name: "REPLACE", Args: "text, position, length, new_text", Desc: "Replace characters at a position", Min: 4, Max: 4,94- eval: func(args []Node, get lookup) Value {95- s, err := textArg(args[0], get)96- if err != nil {97- return *err98- }99- pos, err := intArg(args, 1, 1, get)100- if err != nil {101- return *err102- }103- n, err := intArg(args, 2, 0, get)104- if err != nil {105- return *err106- }107- repl, err := textArg(args[3], get)108- if err != nil {109- return *err110- }111- if pos < 1 || n < 0 {112- return ErrValue113- }114- r := []rune(s)115- from := min(pos-1, len(r))116- to := min(from+n, len(r))117- return capText(string(r[:from]) + repl + string(r[to:]))118- }},38+ eval: replaceAt},11939 &FuncDef{Name: "FIND", Args: "search_for, text_to_search, [starting_at]", Desc: "Position of text, case-sensitive", Min: 2, Max: 3,12040 eval: textFinder(false)},12141 &FuncDef{Name: "SEARCH", Args: "search_for, text_to_search, [starting_at]", Desc: "Position of text, ignoring case, with * and ? wildcards", Min: 2, Max: 3,12242 eval: textFinder(true)},12343 &FuncDef{Name: "TEXT", Args: "number, format", Desc: `A number as text in a format, e.g. "$#,##0.00" or "yyyy-mm-dd"`, Min: 2, Max: 2,124- eval: func(args []Node, get lookup) Value {125- v := eval(args[0], get)126- if v.Kind == Error {127- return v128- }129- pat, err := textArg(args[1], get)130- if err != nil {131- return *err132- }133- if v.Kind == Text {134- n, _, ok := ParseValue(v.Str)135- if !ok {136- return v137- }138- v = num(n)139- }140- return str(FormatPattern(v.Num, pat))141- }},44+ eval: textFormat},14245 &FuncDef{Name: "VALUE", Args: "text", Desc: "Text as a number; dates and times too", Min: 1, Max: 1,143- eval: func(args []Node, get lookup) Value {144- v := eval(args[0], get)145- switch v.Kind {146- case Error, Number:147- return v148- case Empty:149- return num(0)150- case Bool:151- return ErrValue152- }153- if n, _, ok := ParseValue(strings.TrimSpace(v.Str)); ok {154- return num(n)155- }156- if strings.TrimSpace(v.Str) == "" {157- return num(0)158- }159- return ErrValue160- }},46+ eval: valueOf},16147 &FuncDef{Name: "REPT", Args: "text, number_of_repetitions", Desc: "Text repeated a number of times", Min: 2, Max: 2,162- eval: func(args []Node, get lookup) Value {163- s, err := textArg(args[0], get)164- if err != nil {165- return *err166- }167- n, err := intArg(args, 1, 0, get)168- if err != nil {169- return *err170- }171- if n < 0 || n*len(s) > maxText {172- return ErrValue173- }174- return str(strings.Repeat(s, n))175- }},48+ eval: rept},17649 &FuncDef{Name: "EXACT", Args: "string1, string2", Desc: "TRUE if two texts are identical, case included", Min: 2, Max: 2,177- eval: func(args []Node, get lookup) Value {178- a, err := textArg(args[0], get)179- if err != nil {180- return *err181- }182- b, err := textArg(args[1], get)183- if err != nil {184- return *err185- }186- return boolean(a == b)187- }},50+ eval: exact},18851 )18952 }19053 54+// Evaluators for the table above, in its order.55+56+func concatenate(args []Node, get lookup) Value {57+ parts, err := texts(args, get)58+ if err != nil {59+ return *err60+ }61+ return str(strings.Join(parts, ""))62+}63+64+func concat(args []Node, get lookup) Value {65+ a, err := textArg(args[0], get)66+ if err != nil {67+ return *err68+ }69+ b, err := textArg(args[1], get)70+ if err != nil {71+ return *err72+ }73+ return str(a + b)74+}75+76+func textJoin(args []Node, get lookup) Value {77+ delim, err := textArg(args[0], get)78+ if err != nil {79+ return *err80+ }81+ skip, err := boolArg(args, 1, true, get)82+ if err != nil {83+ return *err84+ }85+ parts, err := texts(args[2:], get)86+ if err != nil {87+ return *err88+ }89+ if skip {90+ parts = slicesDeleteEmpty(parts)91+ }92+ return capText(strings.Join(parts, delim))93+}94+95+func mid(args []Node, get lookup) Value {96+ s, err := textArg(args[0], get)97+ if err != nil {98+ return *err99+ }100+ start, err := intArg(args, 1, 1, get)101+ if err != nil {102+ return *err103+ }104+ n, err := intArg(args, 2, 0, get)105+ if err != nil {106+ return *err107+ }108+ if start < 1 || n < 0 {109+ return ErrValue110+ }111+ r := []rune(s)112+ if start > len(r) {113+ return str("")114+ }115+ return str(string(r[start-1 : min(start-1+n, len(r))]))116+}117+118+func trimSpaces(s string) Value {119+ return str(strings.Join(strings.FieldsFunc(s, func(r rune) bool { return r == ' ' }), " "))120+}121+122+func replaceAt(args []Node, get lookup) Value {123+ s, err := textArg(args[0], get)124+ if err != nil {125+ return *err126+ }127+ pos, err := intArg(args, 1, 1, get)128+ if err != nil {129+ return *err130+ }131+ n, err := intArg(args, 2, 0, get)132+ if err != nil {133+ return *err134+ }135+ repl, err := textArg(args[3], get)136+ if err != nil {137+ return *err138+ }139+ if pos < 1 || n < 0 {140+ return ErrValue141+ }142+ r := []rune(s)143+ from := min(pos-1, len(r))144+ to := min(from+n, len(r))145+ return capText(string(r[:from]) + repl + string(r[to:]))146+}147+148+func textFormat(args []Node, get lookup) Value {149+ v := eval(args[0], get)150+ if v.Kind == Error {151+ return v152+ }153+ pat, err := textArg(args[1], get)154+ if err != nil {155+ return *err156+ }157+ if v.Kind == Text {158+ n, _, ok := ParseValue(v.Str)159+ if !ok {160+ return v161+ }162+ v = num(n)163+ }164+ return str(FormatPattern(v.Num, pat))165+}166+167+func valueOf(args []Node, get lookup) Value {168+ v := eval(args[0], get)169+ switch v.Kind {170+ case Error, Number:171+ return v172+ case Empty:173+ return num(0)174+ case Bool:175+ return ErrValue176+ }177+ if n, _, ok := ParseValue(strings.TrimSpace(v.Str)); ok {178+ return num(n)179+ }180+ if strings.TrimSpace(v.Str) == "" {181+ return num(0)182+ }183+ return ErrValue184+}185+186+func rept(args []Node, get lookup) Value {187+ s, err := textArg(args[0], get)188+ if err != nil {189+ return *err190+ }191+ n, err := intArg(args, 1, 0, get)192+ if err != nil {193+ return *err194+ }195+ if n < 0 || n*len(s) > maxText {196+ return ErrValue197+ }198+ return str(strings.Repeat(s, n))199+}200+201+func exact(args []Node, get lookup) Value {202+ a, err := textArg(args[0], get)203+ if err != nil {204+ return *err205+ }206+ b, err := textArg(args[1], get)207+ if err != nil {208+ return *err209+ }210+ return boolean(a == b)211+}212+191213 func capText(s string) Value {192214 if len(s) > maxText {193215 return ErrValue