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>
| M | docs/ | 15 | ++- |
| M | internal/ | 1 | + |
| A | internal/ | 184 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 32 | +++++ |
@@ -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 |@@ -29,6 +29,7 @@ var categories = []struct{ file, title string }{
2929 {"text.go", "Text"},3030 {"lookup.go", "Lookup"},3131 {"dynamic.go", "Arrays"},32+ {"lambda.go", "LET and LAMBDA"},3233 {"date.go", "Date and time"},3334 {"finance.go", "Finance"},3435 {"link.go", "Links"},@@ -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+}@@ -0,0 +1,32 @@
1+package sheet2+3+import "testing"4+5+func TestLetAndLambda(t *testing.T) {6+ s := fixture(t)7+ if err := s.DefineName("x", rect("A1:A1")); err != nil {8+ t.Fatal(err)9+ }10+ checkArrays(t, s, []arrCase{11+ {"=LET(x, 5, x*2)", []string{"10"}},12+ {"=LET(a, 2, b, a+1, a*b)", []string{"6"}},13+ {"=LET(r, C1:C5, SUM(r))", []string{"15"}},14+ {"=LET(s, SEQUENCE(3), s*s)", []string{"1", "4", "9"}},15+ {"=x+1", []string{"11"}}, // the named range, outside LET16+ {"=LET(x, 1, x) + x", []string{"11"}},17+ {"=LAMBDA(v, v*3)(4)", []string{"12"}},18+ {"=LAMBDA(a, b, a-b)(10, 4)", []string{"6"}},19+ {"=LAMBDA(a, b, a-b)(10)", []string{"#N/A"}},20+ {"=LET(double, LAMBDA(v, v*2), double(21))", []string{"42"}},21+ {"=LAMBDA(v, v)", []string{"#VALUE!"}},22+ {"=MAP(C1:C3, LAMBDA(v, v*10))", []string{"10", "20", "30"}},23+ {"=MAP(C1:C2, G1:G2, LAMBDA(a, b, a+b))", []string{"2", "4"}},24+ {"=MAP(C1:C3, LAMBDA(a, b, a))", []string{"#N/A"}},25+ {"=REDUCE(0, C1:C5, LAMBDA(acc, v, acc+v))", []string{"15"}},26+ {"=SCAN(0, C1:C4, LAMBDA(acc, v, acc+v))", []string{"1", "3", "6", "10"}},27+ {"=BYROW(C1:E2, LAMBDA(row, SUM(row)))", []string{"2.5", "4.5"}},28+ {"=BYCOL(C1:E2, LAMBDA(col, COUNT(col)))", []string{"2|0|2"}},29+ {"=BYROW(C1:C2, LAMBDA(row, SEQUENCE(2)))", []string{"#VALUE!", "#VALUE!"}},30+ {"=MAKEARRAY(2, 3, LAMBDA(r, c, r*c))", []string{"1|2|3", "2|4|6"}},31+ })32+}