cat / 012
clone

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

Add LET, LAMBDA, MAP, REDUCE, SCAN, BYROW, BYCOL and MAKEARRAY

Dami committed fdaa8e9
LET binds names to values, or to references and ranges, which then read
as they would where the name is used. LAMBDA's value is a function of
names, called with arguments right away, through a name LET gave it, or
by the functions that call one for each entry, row or column of an
array. A LAMBDA that isn't called is #VALUE!, and one called with the
wrong number of arguments #N/A, as in Sheets.

Co-Authored-By: Claude <noreply@anthropic.com>
commit
fdaa8e901b66f43263029f888b46398a15973d64
parent
b2538b52ba10448bdeca16053a35e233a7f6bb68
author
Dami <dami.etoile@gmail.com>
4 files changed, +231 -1
Mdocs/functions.md15++-
Minternal/functions/doc_test.go1+
Ainternal/functions/lambda.go184++++++++++++++++++++++++++++++
Ainternal/sheet/functions_lambda_test.go32+++++
M docs/functions.md +14 -1
@@ -2,7 +2,7 @@
22 33 <!-- Generated from the engine's function table by TestFunctionsDoc; do not edit. -->44 5-012 has 130 functions. They follow Google Sheets' names, arguments and5+012 has 138 functions. They follow Google Sheets' names, arguments and66 semantics; `[brackets]` mark optional arguments. Function names are77 case-insensitive, and 1-2-3's `@SUM(A1..A5)` spelling still works.88
@@ -143,6 +143,19 @@ Aliases: `AVG` for `AVERAGE`.
143143 | `TRANSPOSE(array_or_range)` | Rows as columns and columns as rows |144144 | `UNIQUE(range, [by_column], [exactly_once])` | The distinct rows (or columns) of a range, in order |145145 146+## LET and LAMBDA147+148+| Function | Description |149+|---|---|150+| `BYCOL(array_or_range, LAMBDA)` | Each column of an array passed to a LAMBDA, one value per column |151+| `BYROW(array_or_range, LAMBDA)` | Each row of an array passed to a LAMBDA, one value per row |152+| `LAMBDA([name, ...], formula_expression)` | A function of names, called with values: LAMBDA(x, x*2)(3) |153+| `LET(name1, value_expression1, [name2, value_expression2, ...], formula_expression)` | Name values for use in a formula |154+| `MAKEARRAY(rows, columns, LAMBDA)` | An array of a size, each entry a LAMBDA of its row and column |155+| `MAP(array1, [array2, ...], LAMBDA)` | Each entry of arrays passed to a LAMBDA |156+| `REDUCE(initial_value, array_or_range, LAMBDA)` | An array folded into one value by a LAMBDA of the total so far and each entry |157+| `SCAN(initial_value, array_or_range, LAMBDA)` | The running totals of REDUCE, one for each entry |158+146159 ## Date and time147160 148161 | Function | Description |
M internal/functions/doc_test.go +1 -0
A internal/functions/lambda.go +184 -0
@@ -0,0 +1,184 @@
1+package functions2+3+import (4+ "github.com/FelineStateMachine/012/internal/formula"5+ "github.com/FelineStateMachine/012/internal/value"6+)7+8+// LET names values within a formula, and LAMBDA makes a function of9+// names, called with arguments right away (LAMBDA(x, x*2)(3)), through a10+// name LET gave it, or by MAP, REDUCE, BYROW, BYCOL, SCAN and MAKEARRAY11+// for each entry of an array. The parser turns the names into Local12+// nodes (formula), so named ranges never replace them.13+14+func init() {15+ define(16+ &FuncDef{Name: "LET", Args: "name1, value_expression1, [name2, value_expression2, ...], formula_expression", Desc: "Name values for use in a formula", Min: 3, Max: -1, step: 2,17+ eval: let, arrays: takesArrays, binds: formula.BindLet, format: func(args []Node, infer func(Node) Format) Format { return infer(args[len(args)-1]) }},18+ &FuncDef{Name: "LAMBDA", Args: "[name, ...], formula_expression", Desc: "A function of names, called with values: LAMBDA(x, x*2)(3)", Min: 1, Max: -1,19+ eval: makeLambda, arrays: takesArrays, binds: formula.BindLambda},20+ &FuncDef{Name: "MAP", Args: "array1, [array2, ...], LAMBDA", Desc: "Each entry of arrays passed to a LAMBDA", Min: 2, Max: -1,21+ eval: mapArrays, arrays: takesArrays},22+ &FuncDef{Name: "REDUCE", Args: "initial_value, array_or_range, LAMBDA", Desc: "An array folded into one value by a LAMBDA of the total so far and each entry", Min: 3, Max: 3,23+ eval: func(args []Node, get lookup) Value { return fold(args, get, false) }, arrays: takesArrays},24+ &FuncDef{Name: "SCAN", Args: "initial_value, array_or_range, LAMBDA", Desc: "The running totals of REDUCE, one for each entry", Min: 3, Max: 3,25+ eval: func(args []Node, get lookup) Value { return fold(args, get, true) }, arrays: takesArrays},26+ &FuncDef{Name: "BYROW", Args: "array_or_range, LAMBDA", Desc: "Each row of an array passed to a LAMBDA, one value per row", Min: 2, Max: 2,27+ eval: func(args []Node, get lookup) Value { return byLine(args, get, true) }, arrays: takesArrays},28+ &FuncDef{Name: "BYCOL", Args: "array_or_range, LAMBDA", Desc: "Each column of an array passed to a LAMBDA, one value per column", Min: 2, Max: 2,29+ eval: func(args []Node, get lookup) Value { return byLine(args, get, false) }, arrays: takesArrays},30+ &FuncDef{Name: "MAKEARRAY", Args: "rows, columns, LAMBDA", Desc: "An array of a size, each entry a LAMBDA of its row and column", Min: 3, Max: 3,31+ eval: makeArray, arrays: takesArrays},32+ )33+}34+35+func let(args []Node, get lookup) Value {36+ outer := len(get.scope)37+ defer func() { get.scope = get.scope[:outer] }()38+ last := len(args) - 139+ for i := 0; i < last; i += 2 {40+ b := get.bind(args[i].(formula.Local).Name, args[i+1])41+ if b.ref == nil && b.val.Kind == value.Error {42+ return b.val43+ }44+ get.scope = append(get.scope, b)45+ }46+ return eval(args[last], get)47+}48+49+func makeLambda(args []Node, get lookup) Value {50+ l := &lambda{body: args[len(args)-1], scope: append([]binding(nil), get.scope...)}51+ for _, a := range args[:len(args)-1] {52+ l.params = append(l.params, a.(formula.Local).Name)53+ }54+ return get.handle(l)55+}56+57+func mapArrays(args []Node, get lookup) Value {58+ last := len(args) - 159+ l, err := lambdaArg(args[last], get, last)60+ if err != nil {61+ return *err62+ }63+ arrs := make([]*Array, last)64+ var s shape65+ for i, a := range args[:last] {66+ if arrs[i], err = arrayArg(a, get); err != nil {67+ return *err68+ }69+ s.fit(arrs[i])70+ }71+ s.drows, s.dcols = s.rows, s.cols72+ out, ok := s.newArray()73+ if !ok {74+ return value.ErrValue75+ }76+ vals := make([]Value, last)77+ for r := range s.rows {78+ for c := range s.cols {79+ for i, a := range arrs {80+ vals[i] = a.bcast(r, c)81+ }82+ out.Set(r, c, get.single(get.call(l, vals...)))83+ }84+ }85+ return get.arrayValue(out)86+}87+88+// fold is REDUCE, or SCAN with running: the LAMBDA called with the total89+// so far and each entry, row by row.90+func fold(args []Node, get lookup, running bool) Value {91+ acc := eval1(args[0], get)92+ a, err := arrayArg(args[1], get)93+ if err != nil {94+ return *err95+ }96+ l, err := lambdaArg(args[2], get, 2)97+ if err != nil {98+ return *err99+ }100+ if tooBig(a.Rows, a.Cols) {101+ return value.ErrValue102+ }103+ var out *Array104+ if running {105+ out = NewArray(a.Rows, a.Cols)106+ }107+ for r := range a.Rows {108+ for c := range a.Cols {109+ acc = get.call(l, acc, a.At(r, c))110+ if running {111+ acc = get.single(acc)112+ out.Set(r, c, acc)113+ }114+ }115+ }116+ if running {117+ return get.arrayValue(out)118+ }119+ return acc120+}121+122+// byLine is BYROW (rows) or BYCOL: the LAMBDA called with each line of123+// the array, one value for each.124+func byLine(args []Node, get lookup, rows bool) Value {125+ a, err := arrayArg(args[0], get)126+ if err != nil {127+ return *err128+ }129+ l, err := lambdaArg(args[1], get, 1)130+ if err != nil {131+ return *err132+ }133+ n, width := a.Rows, a.Cols134+ if !rows {135+ n, width = a.Cols, a.Rows136+ }137+ if tooBig(n, width) {138+ return value.ErrValue139+ }140+ out := NewArray(n, 1)141+ if !rows {142+ out = NewArray(1, n)143+ }144+ for i := range n {145+ line := NewArray(1, width)146+ if !rows {147+ line = NewArray(width, 1)148+ }149+ for j := range width {150+ if rows {151+ line.Set(0, j, a.At(i, j))152+ } else {153+ line.Set(j, 0, a.At(j, i))154+ }155+ }156+ out.V[i] = get.single(get.call(l, get.arrayValue(line)))157+ }158+ return get.arrayValue(out)159+}160+161+func makeArray(args []Node, get lookup) Value {162+ rows, err := intArg(args, 0, 0, get)163+ if err != nil {164+ return *err165+ }166+ cols, err := intArg(args, 1, 0, get)167+ if err != nil {168+ return *err169+ }170+ l, err := lambdaArg(args[2], get, 2)171+ if err != nil {172+ return *err173+ }174+ if rows < 1 || cols < 1 || tooBig(rows, cols) {175+ return value.ErrValue176+ }177+ out := NewArray(rows, cols)178+ for r := range rows {179+ for c := range cols {180+ out.Set(r, c, get.single(get.call(l, num(float64(r+1)), num(float64(c+1)))))181+ }182+ }183+ return get.arrayValue(out)184+}
A internal/sheet/functions_lambda_test.go +32 -0