Ranges and array literals are arrays in array contexts: ARRAYFORMULA, arguments that take ranges (SUM(B2:B4*2), SUMPRODUCT(A1:A3*B1:B3)) and array functions. Operators work element by element, broadcasting a row or column, and in an array context a function of one value is applied to each entry of the arrays it reads as one value, found by calling it with its arguments standing in, so VLOOKUP and SUMIF map over their keys and not their tables. Elsewhere a range still reads as the cell in the formula's row or column, and an array as its first entry. Arrays travel as a Value of a new kind indexing the Reader's arena, so a cell's Value stays as small as it was, and a whole column read as an array keeps only the rows holding data, the rest one fill value. A formula computing an array spills it into the cells to its right and below: spilled cells are derived cells like a pivot's results, written after each evaluation pass outside the undo history, with what reads them recalculated. When cells are in the way, or the array would pass the sheet's edge or max-cells, the anchor shows #REF! with Sheets' explanation, and changing a cell in the way has it try again. Set refuses spilled cells; formatting them keeps the formatting; files keep only the anchor. FILTER, SORT, SORTN, UNIQUE, SEQUENCE, TRANSPOSE, FLATTEN, CHOOSECOLS, CHOOSEROWS and ARRAYFORMULA join the library, and INDEX with a row or column of 0 returns the whole column or row. Co-Authored-By: Claude <noreply@anthropic.com>
- commit
- b2538b52ba10448bdeca16053a35e233a7f6bb68
- parent
- 81f1a2aa9e01d964dd6b6299ff8ea62c6516c276
- author
- Dami <dami.etoile@gmail.com>
@@ -2,7 +2,7 @@
22 33 <!-- Generated from the engine's function table by TestFunctionsDoc; do not edit. -->44 5-012 has 120 functions. They follow Google Sheets' names, arguments and5+012 has 130 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 @@ -128,6 +128,21 @@ Aliases: `AVG` for `AVERAGE`.
128128 | `VLOOKUP(search_key, range, index, [is_sorted])` | Find a key in the first column and return a value from its row |129129 | `XLOOKUP(search_key, lookup_range, result_range, [missing_value], [match_mode], [search_mode])` | Find a key and return the matching entry of another range |130130 131+## Arrays132+133+| Function | Description |134+|---|---|135+| `ARRAYFORMULA(array_formula)` | Compute a formula over arrays: ranges read whole, and functions of one value applied to each entry |136+| `CHOOSECOLS(array, col_num1, [col_num2, ...])` | Columns of an array by position, negative from the end |137+| `CHOOSEROWS(array, row_num1, [row_num2, ...])` | Rows of an array by position, negative from the end |138+| `FILTER(range, condition1, [condition2, ...])` | The rows (or columns) of a range where every condition is true |139+| `FLATTEN(range1, [range2, ...])` | Every entry of ranges in one column, row by row |140+| `SEQUENCE(rows, [columns], [start], [step])` | An array of numbers counting up from start by step |141+| `SORT(range, [sort_column], [is_ascending], [sort_column2, is_ascending2, ...])` | The rows of a range sorted by columns |142+| `SORTN(range, [n], [display_ties_mode], [sort_column1, is_ascending1, ...])` | The first n rows of a range after sorting |143+| `TRANSPOSE(array_or_range)` | Rows as columns and columns as rows |144+| `UNIQUE(range, [by_column], [exactly_once])` | The distinct rows (or columns) of a range, in order |145+131146 ## Date and time132147 133148 | Function | Description |@@ -13,7 +13,7 @@ import (
1313 // meet, as Sheets does.1414 1515 func numArg(n Node, get lookup) (float64, *Value) {16- v := eval(n, get)16+ v := eval1(n, get)1717 if v.Kind == value.Error {1818 return 0, errOf(v)1919 }@@ -51,7 +51,7 @@ func intArg(args []Node, i int, def float64, get lookup) (int, *Value) {
5151 }5252 5353 func textArg(n Node, get lookup) (string, *Value) {54- v := eval(n, get)54+ v := eval1(n, get)5555 if v.Kind == value.Error {5656 return "", errOf(v)5757 }@@ -99,16 +99,37 @@ func (m matrix) dataLen() int {
9999 }100100 101101 func matrixArg(n Node, get lookup) matrix {102- switch n := n.(type) {102+ switch n := get.refOf(n).(type) {103103 case formula.Range:104104 return rectMatrix(n.Sheet, n.Rect, get)105105 case formula.Ref:106106 return rectMatrix(n.Sheet, Rect{From: n.Addr, To: n.Addr}, get)107107 }108- v := evalArray(n, get)108+ v := wholeArg(n, get)109+ if a := get.arrayOf(v); a != nil {110+ return arrayMatrix(a)111+ }112+ if v.Kind == value.Array {113+ v = value.ErrValue // a LAMBDA114+ }109115 return matrix{rows: 1, cols: 1, cell: func(int, int) Value { return v }, dataRows: 1, dataCols: 1, blank: v}110116 }111117 118+// arrayMatrix is an array as a block of values.119+func arrayMatrix(a *Array) matrix {120+ return matrix{rows: a.Rows, cols: a.Cols, cell: a.At, dataRows: a.DRows, dataCols: a.DCols, blank: a.Fill}121+}122+123+// wholeArg evaluates an argument that takes ranges or arrays, in an124+// array context: the whole of an argument standing in for one being125+// mapped over (liftArg), not its entry.126+func wholeArg(n Node, get lookup) Value {127+ if a, ok := n.(liftArg); ok {128+ return a.value(get)129+ }130+ return evalArr(n, get)131+}132+112133 func rectMatrix(sheet string, r Rect, get lookup) matrix {113134 m := matrix{114135 rows: r.To.Row - r.From.Row + 1, cols: r.To.Col - r.From.Col + 1,@@ -132,6 +153,13 @@ func rectMatrix(sheet string, r Rect, get lookup) matrix {
132153 return m133154 }134155 156+// part is the top-left rows x cols of a block that isn't a range.157+func (m matrix) part(rows, cols int) matrix {158+ m.rows, m.cols = rows, cols159+ m.dataRows, m.dataCols = min(m.dataRows, rows), min(m.dataCols, cols)160+ return m161+}162+135163 // resized returns a block of rows x cols from the same top-left cell, as136164 // SUMIF does with a sum range of a different size.137165 func (m matrix) resized(rows, cols int, get lookup) matrix {@@ -188,7 +216,7 @@ func textsWithBlanks(args []Node, get lookup, limit int) ([]string, *Value) {
188216 }189217 }190218 for _, arg := range args {191- rn, ok := arg.(formula.Range)219+ rn, ok := get.refOf(arg).(formula.Range)192220 if !ok {193221 if e := eachOf(arg, get, func(v Value, _ bool) *Value {194222 if v.Kind == value.Error {@@ -0,0 +1,403 @@
1+package functions2+3+import (4+ "github.com/FelineStateMachine/012/internal/formula"5+ "github.com/FelineStateMachine/012/internal/value"6+)7+8+// Arrays, as Sheets computes them. A range read whole, an array literal9+// ({1,2;3,4}) and what FILTER, SORT, SEQUENCE and the like return are10+// arrays; operators over arrays work element by element, and in an array11+// context (ARRAYFORMULA, or an argument that takes ranges, as in12+// SUM(B2:B4*2)) functions that take one value are applied to each13+// element (lift.go). A formula whose result is an array spills it into14+// the cells to its right and below (the engine's spill.go).15+//16+// While a formula is evaluated, an array travels as a Value of kind17+// value.Array whose Num indexes the Reader's arena, so the Value every18+// cell holds stays as small as it is. Only callers that asked for an19+// array get one (Reader.wantArr); anywhere else it reads as its first20+// element, as Sheets reads an array where one value is wanted.21+22+// Array is the value of a formula that computes several: Rows by Cols23+// entries. Only the top-left DRows by DCols hold values of their own, in24+// V row by row; every other entry is Fill. A whole column read as an25+// array costs what it holds: A:A is 1,048,576 rows, of which the rows26+// holding data are stored, the rest blank.27+type Array struct {28+ Rows, Cols int29+ DRows, DCols int30+ V []Value31+ Fill Value32+}33+34+// maxArray caps how many values an array stores (DRows x DCols), so a35+// formula can't exhaust memory: past it the formula is #VALUE!.36+const maxArray = 1 << 2137+38+// NewArray is a dense array of rows x cols values, all blank.39+func NewArray(rows, cols int) *Array {40+ return &Array{Rows: rows, Cols: cols, DRows: rows, DCols: cols, V: make([]Value, rows*cols)}41+}42+43+// At is the entry at row r and column c, counting from 0.44+func (a *Array) At(r, c int) Value {45+ if r < a.DRows && c < a.DCols {46+ return a.V[r*a.DCols+c]47+ }48+ return a.Fill49+}50+51+// Set stores v at row r and column c of a dense array.52+func (a *Array) Set(r, c int, v Value) { a.V[r*a.DCols+c] = v }53+54+// size is how many entries the array has.55+func (a *Array) size() int { return a.Rows * a.Cols }56+57+// bcast is the entry at r, c of a broadcast to more rows or columns: a58+// single row repeats down, a single column across. Past a's size in a59+// dimension it doesn't broadcast in, the entry is #N/A, as Sheets shows60+// arrays of different sizes lined up.61+func (a *Array) bcast(r, c int) Value {62+ if a.Rows == 1 {63+ r = 064+ }65+ if a.Cols == 1 {66+ c = 067+ }68+ if r >= a.Rows || c >= a.Cols {69+ return value.ErrNA70+ }71+ return a.At(r, c)72+}73+74+// tooBig reports whether an array of rows x cols stored values is past75+// maxArray.76+func tooBig(rows, cols int) bool {77+ return rows < 0 || cols < 0 || rows > 0 && cols > maxArray/rows78+}79+80+// lambda is a LAMBDA's value: its parameters, its expression and the81+// names bound where it was written (lambda.go).82+type lambda struct {83+ params []string84+ body Node85+ scope []binding86+}87+88+// arrayValue is a as a value: its only entry when it has one, else a89+// handle to it in the arena.90+func (rd *Reader) arrayValue(a *Array) Value {91+ if a.Rows == 1 && a.Cols == 1 {92+ return a.At(0, 0)93+ }94+ if a.size() == 0 {95+ return value.ErrNA96+ }97+ return rd.handle(a)98+}99+100+// handle stores x (an *Array or a *lambda) in the arena and returns the101+// value that stands for it.102+func (rd *Reader) handle(x any) Value {103+ rd.arena = append(rd.arena, x)104+ return Value{Kind: value.Array, Num: float64(len(rd.arena) - 1)}105+}106+107+// arrayOf is the array v stands for, or nil when v is one value (or a108+// LAMBDA).109+func (rd *Reader) arrayOf(v Value) *Array {110+ if v.Kind != value.Array {111+ return nil112+ }113+ a, _ := rd.arena[int(v.Num)].(*Array)114+ return a115+}116+117+// lambdaOf is the LAMBDA v stands for, or nil.118+func (rd *Reader) lambdaOf(v Value) *lambda {119+ if v.Kind != value.Array {120+ return nil121+ }122+ l, _ := rd.arena[int(v.Num)].(*lambda)123+ return l124+}125+126+// first is v as one value: an array's first entry. A LAMBDA isn't a127+// value a cell can show: #VALUE!, as a LAMBDA not called with arguments128+// is in Sheets.129+func (rd *Reader) first(v Value) Value {130+ if v.Kind != value.Array {131+ return v132+ }133+ if a := rd.arrayOf(v); a != nil {134+ return a.At(0, 0)135+ }136+ return value.ErrValue137+}138+139+// reduce is v as the caller wants it: an array only if it asked for one.140+func (rd *Reader) reduce(v Value) Value {141+ if v.Kind == value.Array && !rd.wantArr {142+ return rd.first(v)143+ }144+ return v145+}146+147+// eval1 evaluates n where one value is wanted, whatever the context: a148+// range reads as one of its cells and an array as its first entry.149+func eval1(n Node, get lookup) Value {150+ if get.lift == 0 && !get.wantArr {151+ return eval(n, get)152+ }153+ lift, want := get.lift, get.wantArr154+ get.lift, get.wantArr = 0, false155+ v := eval(n, get)156+ get.lift, get.wantArr = lift, want157+ return v158+}159+160+// evalArr evaluates n in an array context: ranges read whole, operators161+// work element by element and functions of one value are applied to each162+// element. Arguments that take ranges (SUM(B2:B4*2)) and array functions163+// (FILTER, SORT) read theirs this way.164+func evalArr(n Node, get lookup) Value {165+ lift, want := get.lift, get.wantArr166+ get.lift, get.wantArr = lift+1, true167+ v := eval(n, get)168+ get.lift, get.wantArr = lift, want169+ return v170+}171+172+// arrayArg is argument n read as an array: a range, an array, or one173+// value as a 1x1 array.174+func arrayArg(n Node, get lookup) (*Array, *Value) {175+ v := evalArr(n, get)176+ if a := get.arrayOf(v); a != nil {177+ return a, nil178+ }179+ if v.Kind == value.Error {180+ return nil, errOf(v)181+ }182+ if v.Kind == value.Array { // a LAMBDA183+ return nil, &value.ErrValue184+ }185+ a := NewArray(1, 1)186+ a.V[0] = v187+ return a, nil188+}189+190+// rangeArray reads the range r on sheet as an array: its cells up to191+// the last one holding something, blank past them.192+func (rd *Reader) rangeArray(sheet string, r Rect) Value {193+ if r.From == r.To {194+ return rd.cell(sheet, r.From)195+ }196+ a := &Array{Rows: r.To.Row - r.From.Row + 1, Cols: r.To.Col - r.From.Col + 1}197+ b, any, exists := rd.book.Bounds(sheet, r)198+ switch {199+ case !exists:200+ return value.ErrRef201+ case rd.dense:202+ a.DRows, a.DCols = a.Rows, a.Cols203+ case any:204+ a.DRows, a.DCols = b.To.Row-r.From.Row+1, b.To.Col-r.From.Col+1205+ }206+ if tooBig(a.DRows, a.DCols) {207+ return value.ErrValue208+ }209+ a.V = make([]Value, a.DRows*a.DCols)210+ data := Rect{From: r.From, To: Addr{Col: r.From.Col + a.DCols - 1, Row: r.From.Row + a.DRows - 1}}211+ if a.DRows > 0 && a.DCols > 0 {212+ rd.cells(sheet, data, func(at Addr, v Value) bool {213+ a.Set(at.Row-r.From.Row, at.Col-r.From.Col, v)214+ return true215+ })216+ }217+ return rd.arrayValue(a)218+}219+220+// literal evaluates an array literal: each element in an array context,221+// elements side by side in a row and rows stacked, so {A1:A3,B1:B3} is222+// three rows of two. Rows of different widths are #VALUE!, as Sheets223+// says an array literal is missing values.224+func (rd *Reader) literal(n formula.Array) Value {225+ var rows []*Array226+ for _, row := range n.Rows {227+ var parts []*Array228+ for _, e := range row {229+ a, err := arrayArg(e, rd)230+ if err != nil {231+ return *err232+ }233+ parts = append(parts, a)234+ }235+ joined, ok := hstack(parts)236+ if !ok {237+ return value.ErrValue238+ }239+ rows = append(rows, joined)240+ }241+ a, ok := vstack(rows)242+ if !ok {243+ return value.ErrValue244+ }245+ return rd.arrayValue(a)246+}247+248+// hstack puts arrays of the same height side by side.249+func hstack(parts []*Array) (*Array, bool) {250+ if len(parts) == 1 {251+ return parts[0], true252+ }253+ rows, cols := parts[0].Rows, 0254+ for _, p := range parts {255+ if p.Rows != rows {256+ return nil, false257+ }258+ cols += p.Cols259+ }260+ if tooBig(rows, cols) {261+ return nil, false262+ }263+ out := NewArray(rows, cols)264+ at := 0265+ for _, p := range parts {266+ for r := range rows {267+ for c := range p.Cols {268+ out.Set(r, at+c, p.At(r, c))269+ }270+ }271+ at += p.Cols272+ }273+ return out, true274+}275+276+// vstack stacks arrays of the same width.277+func vstack(parts []*Array) (*Array, bool) {278+ if len(parts) == 1 {279+ return parts[0], true280+ }281+ rows, cols := 0, parts[0].Cols282+ for _, p := range parts {283+ if p.Cols != cols {284+ return nil, false285+ }286+ rows += p.Rows287+ }288+ if tooBig(rows, cols) {289+ return nil, false290+ }291+ out := NewArray(rows, cols)292+ at := 0293+ for _, p := range parts {294+ for r := range p.Rows {295+ for c := range cols {296+ out.Set(at+r, c, p.At(r, c))297+ }298+ }299+ at += p.Rows300+ }301+ return out, true302+}303+304+// shape is the size of arrays lined up for an element-wise operation,305+// and how much of it holds values of its own.306+type shape struct{ rows, cols, drows, dcols int }307+308+// fit widens s to take in a: sizes must match, or be 1 to broadcast.309+func (s *shape) fit(a *Array) bool {310+ if a == nil {311+ return true312+ }313+ if s.rows == 0 {314+ *s = shape{rows: a.Rows, cols: a.Cols}315+ }316+ var ok bool317+ if s.rows, ok = fitDim(s.rows, a.Rows); !ok {318+ return false319+ }320+ if s.cols, ok = fitDim(s.cols, a.Cols); !ok {321+ return false322+ }323+ return true324+}325+326+// fitDim is the length two broadcast dimensions line up to: the longer,327+// when one is 1 or both are equal. Arrays of different sizes otherwise328+// line up to the longer, the missing entries #N/A (Array.bcast).329+func fitDim(a, b int) (int, bool) {330+ return max(a, b), true331+}332+333+// data sets how much of s holds values of their own, given the arrays334+// lined up in it.335+func (s *shape) data(arrs ...*Array) {336+ s.drows, s.dcols = 0, 0337+ for _, a := range arrs {338+ if a == nil {339+ continue340+ }341+ r, c := a.DRows, a.DCols342+ if a.Rows == 1 && r > 0 {343+ r = s.rows344+ }345+ if a.Cols == 1 && c > 0 {346+ c = s.cols347+ }348+ if a.Rows != s.rows && a.Rows != 1 || a.Cols != s.cols && a.Cols != 1 {349+ r, c = s.rows, s.cols // #N/A past its end350+ }351+ s.drows, s.dcols = max(s.drows, r), max(s.dcols, c)352+ }353+ s.drows, s.dcols = min(s.drows, s.rows), min(s.dcols, s.cols)354+}355+356+// newArray is an array of shape s, its values to fill in.357+func (s shape) newArray() (*Array, bool) {358+ if tooBig(s.drows, s.dcols) {359+ return nil, false360+ }361+ return &Array{Rows: s.rows, Cols: s.cols, DRows: s.drows, DCols: s.dcols, V: make([]Value, s.drows*s.dcols)}, true362+}363+364+// entry is v at r, c: its entry when it is an array, else v itself.365+func entry(a *Array, v Value, r, c int) Value {366+ if a == nil {367+ return v368+ }369+ return a.bcast(r, c)370+}371+372+// fillOf is the value past v's data: its fill when it is an array.373+func fillOf(a *Array, v Value) Value {374+ if a == nil {375+ return v376+ }377+ return a.Fill378+}379+380+// elementwise applies op to the entries of l and r lined up, one of381+// them at least an array.382+func (rd *Reader) elementwise(l, r Value, op func(l, r Value) Value) Value {383+ la, ra := rd.arrayOf(l), rd.arrayOf(r)384+ if la == nil && l.Kind == value.Array || ra == nil && r.Kind == value.Array {385+ return value.ErrValue // a LAMBDA386+ }387+ var s shape388+ if !s.fit(la) || !s.fit(ra) {389+ return value.ErrValue390+ }391+ s.data(la, ra)392+ out, ok := s.newArray()393+ if !ok {394+ return value.ErrValue395+ }396+ for i := range s.drows {397+ for j := range s.dcols {398+ out.Set(i, j, op(entry(la, l, i, j), entry(ra, r, i, j)))399+ }400+ }401+ out.Fill = op(fillOf(la, l), fillOf(ra, r))402+ return rd.arrayValue(out)403+}@@ -0,0 +1,199 @@
1+package functions2+3+import (4+ "slices"5+6+ "github.com/FelineStateMachine/012/internal/value"7+)8+9+// Sorting and comparing the rows of arrays, for SORT, SORTN and UNIQUE10+// (dynamic.go).11+12+// sortKey is a column to sort by and its direction.13+type sortKey struct {14+ col *Array // one column, aligned with the rows15+ desc bool16+}17+18+// sortKeys reads SORT's (sort_column, is_ascending) pairs from args[i]19+// on: a column by position in a, or a range of its own, aligned with it.20+// With none, the first column goes up.21+func sortKeys(a *Array, args []Node, i int, get lookup) ([]sortKey, *Value) {22+ if !given(args, i) && len(args) <= i+1 {23+ return []sortKey{{col: a.column(0)}}, nil24+ }25+ var keys []sortKey26+ for ; i < len(args); i += 2 {27+ k, err := sortKeyArg(a, args[i], get)28+ if err != nil {29+ return nil, err30+ }31+ asc, err := boolArg(args, i+1, true, get)32+ if err != nil {33+ return nil, err34+ }35+ k.desc = !asc36+ keys = append(keys, k)37+ }38+ return keys, nil39+}40+41+func sortKeyArg(a *Array, n Node, get lookup) (sortKey, *Value) {42+ col, err := arrayArg(n, get)43+ switch {44+ case err != nil:45+ return sortKey{}, err46+ case col.Rows == 1 && col.Cols == 1:47+ i, err := toNum(col.At(0, 0))48+ if err != nil {49+ return sortKey{}, err50+ }51+ if i < 1 || int(i) > a.Cols {52+ return sortKey{}, &value.ErrValue53+ }54+ return sortKey{col: a.column(int(i) - 1)}, nil55+ case col.Cols != 1 || col.Rows != a.Rows:56+ return sortKey{}, &value.ErrValue57+ }58+ return sortKey{col: col}, nil59+}60+61+// column is column c of a, as an array of one column.62+func (a *Array) column(c int) *Array {63+ out := &Array{Rows: a.Rows, Cols: 1, Fill: a.Fill}64+ if c < a.DCols {65+ out.DRows, out.DCols = a.DRows, 166+ out.V = make([]Value, a.DRows)67+ for r := range a.DRows {68+ out.V[r] = a.At(r, c)69+ }70+ }71+ return out72+}73+74+// sortOrder orders rows 0..n-1 by keys, stably: numbers, then text, then75+// booleans, ascending or not, and blanks last either way, as Sheets76+// sorts.77+func sortOrder(n int, keys []sortKey) []int {78+ order := make([]int, n)79+ for i := range order {80+ order[i] = i81+ }82+ slices.SortStableFunc(order, func(x, y int) int {83+ for _, k := range keys {84+ if d := sortCompare(k.col.At(x, 0), k.col.At(y, 0), k.desc); d != 0 {85+ return d86+ }87+ }88+ return 089+ })90+ return order91+}92+93+// sortCompare orders two values for sorting, blanks last.94+func sortCompare(a, b Value, desc bool) int {95+ ab, bb := a.Kind == value.Empty, b.Kind == value.Empty96+ switch {97+ case ab || bb:98+ return boolOrder(ab) - boolOrder(bb)99+ case a.Kind == value.Error || b.Kind == value.Error:100+ return boolOrder(a.Kind == value.Error) - boolOrder(b.Kind == value.Error)101+ }102+ d := compare(a, b)103+ if desc {104+ d = -d105+ }106+ return d107+}108+109+func boolOrder(b bool) int {110+ if b {111+ return 1112+ }113+ return 0114+}115+116+// sortedData sorts a's rows that hold data (every row past them is117+// blank, and blanks sort last), keeping the blank rows as its fill.118+func sortedData(a *Array, keys []sortKey) *Array {119+ data := a.DRows120+ for _, k := range keys {121+ data = max(data, k.col.DRows)122+ }123+ data = min(data, a.Rows)124+ out := a.rows(sortOrder(data, keys))125+ out.Rows, out.Fill = a.Rows, a.Fill126+ return out127+}128+129+// topRows picks SORTN's rows of sorted, by its display_ties_mode: 0 the130+// first n; 1 the first n and any tied with the last; 2 the first n131+// distinct rows; 3 the rows of the first n distinct keys.132+func topRows(sorted *Array, keys []sortKey, n, mode int) []int {133+ keyed := sortedKeys(sorted, keys)134+ var out []int135+ distinct := 0136+ for r := range sorted.Rows {137+ switch mode {138+ case 0:139+ if len(out) == n {140+ return out141+ }142+ case 1:143+ if len(out) >= n && !sameRow(keyed, r, out[len(out)-1]) {144+ return out145+ }146+ case 2:147+ if slices.ContainsFunc(out, func(p int) bool { return sameRow(sorted, r, p) }) {148+ continue149+ }150+ if len(out) == n {151+ return out152+ }153+ case 3:154+ if len(out) == 0 || !sameRow(keyed, r, out[len(out)-1]) {155+ distinct++156+ }157+ if distinct > n {158+ return out159+ }160+ }161+ out = append(out, r)162+ }163+ return out164+}165+166+// sortedKeys is the sort key columns of sorted's rows side by side, for167+// comparing rows by their keys.168+func sortedKeys(sorted *Array, keys []sortKey) *Array {169+ out := NewArray(sorted.Rows, len(keys))170+ for i, k := range keys {171+ for r := range sorted.Rows {172+ out.Set(r, i, k.col.At(r, 0))173+ }174+ }175+ return out176+}177+178+// sameRow reports whether rows x and y of a hold the same values: text179+// compared exactly, as UNIQUE does.180+func sameRow(a *Array, x, y int) bool {181+ for c := range a.Cols {182+ if !sameValue(a.At(x, c), a.At(y, c)) {183+ return false184+ }185+ }186+ return true187+}188+189+// sameValue reports whether two values are the same for UNIQUE: of one190+// kind and equal, text case included, as Sheets compares them.191+func sameValue(a, b Value) bool {192+ if a.Kind != b.Kind {193+ return false194+ }195+ if a.Kind == value.Text || a.Kind == value.Error {196+ return a.Str == b.Str197+ }198+ return a.Num == b.Num199+}@@ -304,15 +304,29 @@ func Decimalize(n Node) Node {
304304 n.L, n.R = Decimalize(n.L), Decimalize(n.R)305305 return decBinary(n)306306 case formula.Call:307- args := make([]Node, len(n.Args))308- for i, a := range n.Args {309- args[i] = Decimalize(a)310- }311- n.Args = args307+ n.Args = decimalizeAll(n.Args)312308 if twin, ok := decFuncs()[funcOf(n).Name]; ok {313309 n.Fn = twin314310 }315311 return n312+ case formula.Invoke:313+ n.Fn = Decimalize(n.Fn)314+ n.Args = decimalizeAll(n.Args)315+ return n316+ case formula.Array:317+ rows := make([][]Node, len(n.Rows))318+ for i, row := range n.Rows {319+ rows[i] = decimalizeAll(row)320+ }321+ return formula.Array{Rows: rows}316322 }317323 return n318324 }325+326+func decimalizeAll(ns []Node) []Node {327+ out := make([]Node, len(ns))328+ for i, a := range ns {329+ out[i] = Decimalize(a)330+ }331+ return out332+}@@ -28,6 +28,7 @@ var categories = []struct{ file, title string }{
2828 {"logic.go", "Logic and information"},2929 {"text.go", "Text"},3030 {"lookup.go", "Lookup"},31+ {"dynamic.go", "Arrays"},3132 {"date.go", "Date and time"},3233 {"finance.go", "Finance"},3334 {"link.go", "Links"},@@ -0,0 +1,355 @@
1+package functions2+3+import (4+ "strconv"5+6+ "github.com/FelineStateMachine/012/internal/value"7+)8+9+// Functions that compute arrays, which spill from the cell into the10+// cells to its right and below. Whole columns cost what they hold: FILTER11+// and SORT of A:A work on its rows holding data, the blank rows past12+// them being one fill (Array.Fill).13+14+func init() {15+ define(16+ &FuncDef{Name: "ARRAYFORMULA", Args: "array_formula", Desc: "Compute a formula over arrays: ranges read whole, and functions of one value applied to each entry", Min: 1, Max: 1,17+ eval: func(args []Node, get lookup) Value { return evalArr(args[0], get) }, arrays: takesArrays, format: inherit},18+ &FuncDef{Name: "FILTER", Args: "range, condition1, [condition2, ...]", Desc: "The rows (or columns) of a range where every condition is true", Min: 2, Max: -1,19+ eval: filter, arrays: takesArrays, format: inheritFrom(0)},20+ &FuncDef{Name: "SORT", Args: "range, [sort_column], [is_ascending], [sort_column2, is_ascending2, ...]", Desc: "The rows of a range sorted by columns", Min: 1, Max: -1,21+ eval: sortRows, arrays: takesArrays, format: inheritFrom(0)},22+ &FuncDef{Name: "SORTN", Args: "range, [n], [display_ties_mode], [sort_column1, is_ascending1, ...]", Desc: "The first n rows of a range after sorting", Min: 1, Max: -1,23+ eval: sortN, arrays: takesArrays, format: inheritFrom(0)},24+ &FuncDef{Name: "UNIQUE", Args: "range, [by_column], [exactly_once]", Desc: "The distinct rows (or columns) of a range, in order", Min: 1, Max: 3,25+ eval: unique, arrays: takesArrays, format: inheritFrom(0)},26+ &FuncDef{Name: "SEQUENCE", Args: "rows, [columns], [start], [step]", Desc: "An array of numbers counting up from start by step", Min: 1, Max: 4,27+ eval: sequence, arrays: takesArrays},28+ &FuncDef{Name: "TRANSPOSE", Args: "array_or_range", Desc: "Rows as columns and columns as rows", Min: 1, Max: 1,29+ eval: transpose, arrays: takesArrays, format: inheritFrom(0)},30+ &FuncDef{Name: "FLATTEN", Args: "range1, [range2, ...]", Desc: "Every entry of ranges in one column, row by row", Min: 1, Max: -1,31+ eval: flatten, arrays: takesArrays, format: inheritFrom(0)},32+ &FuncDef{Name: "CHOOSECOLS", Args: "array, col_num1, [col_num2, ...]", Desc: "Columns of an array by position, negative from the end", Min: 2, Max: -1,33+ eval: func(args []Node, get lookup) Value { return choose2(args, get, false) }, arrays: takesArrays, format: inheritFrom(0)},34+ &FuncDef{Name: "CHOOSEROWS", Args: "array, row_num1, [row_num2, ...]", Desc: "Rows of an array by position, negative from the end", Min: 2, Max: -1,35+ eval: func(args []Node, get lookup) Value { return choose2(args, get, true) }, arrays: takesArrays, format: inheritFrom(0)},36+ )37+}38+39+// Evaluators for the table above, in its order.40+41+func filter(args []Node, get lookup) Value {42+ a, err := arrayArg(args[0], get)43+ if err != nil {44+ return *err45+ }46+ conds := make([]*Array, len(args)-1)47+ for i, n := range args[1:] {48+ if conds[i], err = arrayArg(n, get); err != nil {49+ return *err50+ }51+ }52+ byRow := conds[0].Cols == 1 && conds[0].Rows == a.Rows53+ if !byRow && (conds[0].Rows != 1 || conds[0].Cols != a.Cols) {54+ return value.ErrValue // FILTER has mismatched range sizes55+ }56+ if !byRow {57+ a = a.transposed()58+ for i, c := range conds {59+ conds[i] = c.transposed()60+ }61+ }62+ for _, c := range conds {63+ if c.Rows != a.Rows || c.Cols != 1 {64+ return value.ErrValue65+ }66+ }67+ data := a.DRows68+ for _, c := range conds {69+ data = max(data, c.DRows)70+ }71+ keep, err := passing(conds, data)72+ if err != nil {73+ return *err74+ }75+ out := a.rows(keep)76+ if a.Rows > data { // the rows past every data area: one fill77+ if ok, err := passes1(conds, -1); err != nil {78+ return *err79+ } else if ok {80+ out.Rows += a.Rows - data81+ out.Fill = a.Fill82+ }83+ }84+ if out.Rows == 0 {85+ return value.ErrNA // no matches86+ }87+ if !byRow {88+ out = out.transposed()89+ }90+ return get.arrayValue(out)91+}92+93+// passing is the rows below data where every condition is true.94+func passing(conds []*Array, data int) ([]int, *Value) {95+ var keep []int96+ for r := range data {97+ ok, err := passes1(conds, r)98+ if err != nil {99+ return nil, err100+ }101+ if ok {102+ keep = append(keep, r)103+ }104+ }105+ return keep, nil106+}107+108+// passes1 reports whether every condition is true in row r, or past109+// their data with r < 0.110+func passes1(conds []*Array, r int) (bool, *Value) {111+ for _, c := range conds {112+ v := c.Fill113+ if r >= 0 {114+ v = c.At(r, 0)115+ }116+ if v.Kind == value.Error {117+ return false, errOf(v)118+ }119+ f, err := toNum(v)120+ if err != nil {121+ return false, err122+ }123+ if f == 0 {124+ return false, nil125+ }126+ }127+ return true, nil128+}129+130+// rows is a new array of the rows of a at indexes, as wide as a and131+// holding as many columns of data.132+func (a *Array) rows(indexes []int) *Array {133+ out := &Array{Rows: len(indexes), Cols: a.Cols, DRows: len(indexes), DCols: a.DCols, Fill: a.Fill}134+ out.V = make([]Value, len(indexes)*a.DCols)135+ for i, r := range indexes {136+ for c := range a.DCols {137+ out.Set(i, c, a.At(r, c))138+ }139+ }140+ return out141+}142+143+// transposed is a with rows and columns swapped.144+func (a *Array) transposed() *Array {145+ out := &Array{Rows: a.Cols, Cols: a.Rows, DRows: a.DCols, DCols: a.DRows, Fill: a.Fill, V: make([]Value, len(a.V))}146+ for r := range a.DRows {147+ for c := range a.DCols {148+ out.Set(c, r, a.At(r, c))149+ }150+ }151+ return out152+}153+154+func sortRows(args []Node, get lookup) Value {155+ a, err := arrayArg(args[0], get)156+ if err != nil {157+ return *err158+ }159+ keys, err := sortKeys(a, args, 1, get)160+ if err != nil {161+ return *err162+ }163+ return get.arrayValue(sortedData(a, keys))164+}165+166+func sortN(args []Node, get lookup) Value {167+ a, err := arrayArg(args[0], get)168+ if err != nil {169+ return *err170+ }171+ n, err := intArg(args, 1, 1, get)172+ if err != nil {173+ return *err174+ }175+ mode, err := intArg(args, 2, 0, get)176+ if err != nil {177+ return *err178+ }179+ if n < 0 || mode < 0 || mode > 3 {180+ return value.ErrValue181+ }182+ keys, err := sortKeys(a, args, 3, get)183+ if err != nil {184+ return *err185+ }186+ sorted := sortedData(a, keys)187+ sorted.Rows = sorted.DRows // rows past the data are blank: never among the first188+ rows := topRows(sorted, keys, n, mode)189+ if len(rows) == 0 {190+ return value.ErrNA191+ }192+ return get.arrayValue(sorted.rows(rows))193+}194+195+func unique(args []Node, get lookup) Value {196+ a, err := arrayArg(args[0], get)197+ if err != nil {198+ return *err199+ }200+ byCol, err := boolArg(args, 1, false, get)201+ if err != nil {202+ return *err203+ }204+ once, err := boolArg(args, 2, false, get)205+ if err != nil {206+ return *err207+ }208+ if byCol {209+ a = a.transposed()210+ }211+ n := min(a.DRows+1, a.Rows) // the data rows, and one blank row past them212+ groups := map[string][]int{}213+ var order []string214+ for r := range n {215+ k := rowKey(a, r)216+ if _, seen := groups[k]; !seen {217+ order = append(order, k)218+ }219+ groups[k] = append(groups[k], r)220+ }221+ var keep []int222+ for _, k := range order {223+ g := groups[k]224+ if once && (len(g) > 1 || g[0] == a.DRows && a.Rows > a.DRows+1) {225+ continue // repeated, or the blank row, which repeats past the data226+ }227+ keep = append(keep, g[0])228+ }229+ if len(keep) == 0 {230+ return value.ErrNA231+ }232+ out := a.rows(keep)233+ if byCol {234+ out = out.transposed()235+ }236+ return get.arrayValue(out)237+}238+239+// rowKey is a key equal for rows UNIQUE takes to be the same.240+func rowKey(a *Array, r int) string {241+ var b []byte242+ for c := range a.Cols {243+ v := a.At(r, c)244+ if v.Kind == value.Text && v.Str == "" {245+ v = Value{} // empty text and a blank are the same246+ }247+ b = append(b, byte(v.Kind))248+ if v.Kind == value.Text || v.Kind == value.Error {249+ b = append(b, v.Str...)250+ } else {251+ b = strconv.AppendFloat(b, v.Num, 'g', -1, 64)252+ }253+ b = append(b, 0)254+ }255+ return string(b)256+}257+258+func sequence(args []Node, get lookup) Value {259+ rows, err := intArg(args, 0, 1, get)260+ if err != nil {261+ return *err262+ }263+ cols, err := intArg(args, 1, 1, get)264+ if err != nil {265+ return *err266+ }267+ start, err := optNum(args, 2, 1, get)268+ if err != nil {269+ return *err270+ }271+ step, err := optNum(args, 3, 1, get)272+ if err != nil {273+ return *err274+ }275+ if rows < 1 || cols < 1 || tooBig(rows, cols) {276+ return value.ErrValue277+ }278+ out := NewArray(rows, cols)279+ for i := range out.V {280+ out.V[i] = num(start + float64(i)*step)281+ }282+ return get.arrayValue(out)283+}284+285+func transpose(args []Node, get lookup) Value {286+ a, err := arrayArg(args[0], get)287+ if err != nil {288+ return *err289+ }290+ return get.arrayValue(a.transposed())291+}292+293+func flatten(args []Node, get lookup) Value {294+ var vals []Value295+ var fill Value296+ extra := 0297+ for i, n := range args {298+ a, err := arrayArg(n, get)299+ if err != nil {300+ return *err301+ }302+ rows := a.Rows303+ if i == len(args)-1 {304+ rows = a.DRows // the rest is one fill305+ extra, fill = (a.Rows-a.DRows)*a.Cols, a.Fill306+ }307+ if tooBig(len(vals)+rows, a.Cols) {308+ return value.ErrValue309+ }310+ for r := range rows {311+ for c := range a.Cols {312+ vals = append(vals, a.At(r, c))313+ }314+ }315+ }316+ out := &Array{Rows: len(vals) + extra, Cols: 1, DRows: len(vals), DCols: 1, V: vals, Fill: fill}317+ return get.arrayValue(out)318+}319+320+// choose2 is CHOOSEROWS (rows) or CHOOSECOLS.321+func choose2(args []Node, get lookup, rows bool) Value {322+ a, err := arrayArg(args[0], get)323+ if err != nil {324+ return *err325+ }326+ if !rows {327+ a = a.transposed()328+ }329+ var pick []int330+ for i := 1; i < len(args); i++ {331+ v, err := arrayArg(args[i], get)332+ if err != nil {333+ return *err334+ }335+ for _, x := range v.V {336+ f, err := toNum(x)337+ if err != nil {338+ return *err339+ }340+ k := int(f)341+ if k < 0 {342+ k = a.Rows + k + 1343+ }344+ if k < 1 || k > a.Rows {345+ return value.ErrValue346+ }347+ pick = append(pick, k-1)348+ }349+ }350+ out := a.rows(pick)351+ if !rows {352+ out = out.transposed()353+ }354+ return get.arrayValue(out)355+}@@ -7,24 +7,53 @@ import (
77 "github.com/FelineStateMachine/012/internal/value"88 )99 10-// Eval computes a formula, reading the cells it refers to through get.11-// A range where one value is wanted reads as the cell in the row or12-// column last given to EvalAt (see intersect).13-func Eval(n Node, get *Reader) Value { return eval(n, get) }14-15-// EvalAt computes the formula in the cell at, so that a range used where16-// one value is wanted reads as the cell in at's row or column (implicit17-// intersection, as Sheets and Excel do). Evaluations nest, one cell's18-// formula reading another's through the same Reader, so the cell before19-// is given back afterwards.10+// Eval computes a formula where one value is wanted, reading the cells11+// it refers to through get. A range where one value is wanted reads as12+// the cell in the row or column last given to EvalAt (see intersect),13+// and an array as its first entry.14+func Eval(n Node, get *Reader) Value { return EvalAt(n, get, get.here) }15+16+// EvalAt computes the formula in the cell at as one value, so that a17+// range used where one value is wanted reads as the cell in at's row or18+// column (implicit intersection, as Sheets and Excel do). Evaluations19+// nest, one cell's formula reading another's through the same Reader, so20+// the cell before is given back afterwards.2021 func EvalAt(n Node, get *Reader, at Addr) Value {21- outer := get.here22- get.here = at23- v := eval(n, get)24- get.here = outer22+ v, _ := evalTop(n, get, at, false)2523 return v2624 }2725 26+// EvalCell computes the formula in the cell at, as EvalAt, and the array27+// it computes when that is several values: the cell shows the first, and28+// the engine spills the array from it. A 1x1 array is one value.29+func EvalCell(n Node, get *Reader, at Addr) (Value, *Array) {30+ return evalTop(n, get, at, true)31+}32+33+// evalTop evaluates a cell's formula with a fresh evaluation state,34+// saving the state of the formula it nests in (a cell read while another35+// is evaluated).36+func evalTop(n Node, get *Reader, at Addr, arrays bool) (Value, *Array) {37+ saved := get.evalState38+ get.evalState = evalState{here: at, wantArr: arrays}39+ get.nest++40+ // Deferred, so an evaluation the engine abandons part way (its41+ // evaluate.go) leaves the Reader as it found it.42+ defer get.leave(saved)43+ v := eval(n, get)44+ return get.first(v), get.arrayOf(v)45+}46+47+// leave gives back the evaluation state saved, emptying the arena when48+// the outermost formula is done.49+func (rd *Reader) leave(saved evalState) {50+ if rd.nest--; rd.nest == 0 {51+ clear(rd.arena)52+ rd.arena = rd.arena[:0]53+ }54+ rd.evalState = saved55+}56+2857 func eval(n Node, get lookup) Value {2958 switch n := n.(type) {3059 case formula.Num:@@ -42,6 +71,9 @@ func eval(n Node, get lookup) Value {
4271 case formula.Empty:4372 return Value{}4473 case formula.Range:74+ if get.lift > 0 {75+ return get.rangeArray(n.Sheet, n.Rect)76+ }4577 if a, ok := get.intersect(n.Rect); ok {4678 return get.cell(n.Sheet, a)4779 }@@ -55,13 +87,44 @@ func eval(n Node, get lookup) Value {
5587 case decBinary:5688 return evalBinary(formula.Binary(n), get, true)5789 case formula.Call:58- *get.depth++ // see the engine's evaluate.go; operators count in theirs59- arrays := get.arrays60- get.arrays = 0 // a function's own arguments take one value again61- v := funcOf(n).call(n.Args, get)62- get.arrays = arrays63- *get.depth--64- return v90+ return evalCall(n, get)91+ }92+ return get.reduce(evalOther(n, get))93+}94+95+// evalCall calls a function. In an array context a function of one96+// value is mapped over the arrays it's given (lift.go); otherwise its97+// arguments are read as it reads them, and what it returns is one value98+// unless the caller asked for an array.99+func evalCall(n formula.Call, get lookup) Value {100+ *get.depth++ // see the engine's evaluate.go; operators count in theirs101+ f := funcOf(n)102+ var v Value103+ if get.lift > 0 && f.arrays != takesArrays {104+ v = liftCall(f, n.Args, get)105+ } else {106+ want := get.wantArr107+ get.wantArr = f.arrays != liftScalar108+ v = f.call(n.Args, get)109+ get.wantArr = want110+ }111+ *get.depth--112+ return get.reduce(v)113+}114+115+// evalOther evaluates what formulas rarely hold: array literals, names116+// LET and LAMBDA bind, LAMBDA calls, and arguments standing in for117+// arrays being mapped over.118+func evalOther(n Node, get lookup) Value {119+ switch n := n.(type) {120+ case formula.Array:121+ return get.literal(n)122+ case formula.Local:123+ return get.local(n.Name)124+ case formula.Invoke:125+ return get.invoke(n)126+ case liftArg:127+ return n.one(get)65128 }66129 return value.ErrValue67130 }@@ -72,10 +135,17 @@ func evalUnary(n formula.Unary, get lookup, dec bool) Value {
72135 *get.depth++73136 x := eval(n.X, get)74137 *get.depth--138+ if x.Kind == value.Array {139+ return get.reduce(get.elementwise(x, Value{}, func(x, _ Value) Value { return unaryOp(n.Op, x, dec) }))140+ }141+ return unaryOp(n.Op, x, dec)142+}143+144+func unaryOp(op string, x Value, dec bool) Value {75145 if x.Kind == value.Error {76146 return x77147 }78- switch n.Op {148+ switch op {79149 case "-":80150 f, err := toNum(x)81151 if err != nil {@@ -109,30 +179,24 @@ func evalBinary(n formula.Binary, get lookup, dec bool) Value {
109179 *get.depth++110180 l, r := eval(n.L, get), eval(n.R, get)111181 *get.depth--182+ if l.Kind == value.Array || r.Kind == value.Array {183+ return get.reduce(get.elementwise(l, r, func(l, r Value) Value { return binaryOp(n.Op, l, r, dec) }))184+ }185+ return binaryOp(n.Op, l, r, dec)186+}187+188+func binaryOp(op string, l, r Value, dec bool) Value {112189 if l.Kind == value.Error {113190 return l114191 }115192 if r.Kind == value.Error {116193 return r117194 }118- switch n.Op {195+ switch op {119196 case "&":120197 return Value{Kind: value.Text, Str: text(l) + text(r)}121198 case "=", "<>", "<", ">", "<=", ">=":122- c := compare(l, r)123- switch n.Op {124- case "=":125- return boolean(c == 0)126- case "<>":127- return boolean(c != 0)128- case "<":129- return boolean(c < 0)130- case ">":131- return boolean(c > 0)132- case "<=":133- return boolean(c <= 0)134- }135- return boolean(c >= 0)199+ return boolean(cmpResult(op, compare(l, r)))136200 }137201 a, err := toNum(l)138202 if err != nil {@@ -143,11 +207,11 @@ func evalBinary(n formula.Binary, get lookup, dec bool) Value {
143207 return *err144208 }145209 if dec {146- if v, ok := decArith(n.Op, a, b); ok {210+ if v, ok := decArith(op, a, b); ok {147211 return v148212 }149213 }150- switch n.Op {214+ switch op {151215 case "+":152216 return num(a + b)153217 case "-":@@ -170,12 +234,8 @@ func evalBinary(n formula.Binary, get lookup, dec bool) Value {
170234 }171235 172236 // intersect is the cell a range stands for where one value is wanted173-// (Intersect), in the formula being evaluated. Inside an argument that174-// takes ranges (evalArray) only a single cell does.237+// (Intersect), in the formula being evaluated.175238 func (rd *Reader) intersect(r Rect) (Addr, bool) {176- if rd.arrays > 0 && r.From != r.To {177- return Addr{}, false178- }179239 return Intersect(r, rd.here)180240 }181241 @@ -195,14 +255,3 @@ func Intersect(r Rect, at Addr) (Addr, bool) {
195255 }196256 return Addr{}, false197257 }198-199-// evalArray computes an expression given to an argument that takes200-// ranges (SUM(B2:B4*2), SUMPRODUCT(A1:A3*B1:B3)). Sheets computes those201-// over whole arrays, which 012 doesn't, so ranges in them stay #VALUE!202-// rather than reading one cell each.203-func evalArray(n Node, get lookup) Value {204- get.arrays++205- v := eval(n, get)206- get.arrays--207- return v208-}@@ -29,13 +29,32 @@ type FuncDef struct {
2929 format func(args []Node, infer func(Node) Format) Format3030 // remote builds the question a JEV function asks (functions_jev.go).3131 remote func(args []Node, get lookup) (RemoteCall, error)32+ // arrays is how the function meets arrays (array.go).33+ arrays arrayUse34+ // binds is how its arguments bind names, for LET and LAMBDA.35+ binds formula.Binding3236 }3337 38+// arrayUse is how a function meets arrays.39+type arrayUse uint840+41+const (42+ // liftScalar takes and returns one value; in an array context it is43+ // applied to each entry of the arrays it's given (lift.go).44+ liftScalar arrayUse = iota45+ // liftPass is liftScalar, but what it returns may be an array one of46+ // its arguments computed (IF, IFERROR, CHOOSE, INDEX).47+ liftPass48+ // takesArrays reads arrays itself and may return one (FILTER, SORT,49+ // ARRAYFORMULA, LET): it is never mapped over arrays.50+ takesArrays51+)52+3453 func (f *FuncDef) call(args []Node, get lookup) Value { return f.eval(args, get) }3554 3655 // Signature is how the parser checks calls to f.3756 func (f *FuncDef) Signature() formula.Signature {38- return formula.Signature{Name: f.Name, Args: f.Args, Min: f.Min, Max: f.Max, Step: f.step}57+ return formula.Signature{Name: f.Name, Args: f.Args, Min: f.Min, Max: f.Max, Step: f.step, Binds: f.binds}3958 }4059 4160 // Remote reports whether f asks a remote question (JEV functions).@@ -134,9 +153,9 @@ func init() {
134153 &FuncDef{Name: "TRUE", Desc: "The logical value TRUE", Max: 0, eval: constant(boolean(true))},135154 &FuncDef{Name: "FALSE", Desc: "The logical value FALSE", Max: 0, eval: constant(boolean(false))},136155 &FuncDef{Name: "NA", Desc: "The #N/A error", Max: 0, eval: constant(value.ErrNA)},137- &FuncDef{Name: "IF", Args: "condition, value_if_true, [value_if_false]", Desc: "Choose a value by a condition", Min: 2, Max: 3,156+ &FuncDef{Name: "IF", Args: "condition, value_if_true, [value_if_false]", Desc: "Choose a value by a condition", Min: 2, Max: 3, arrays: liftPass,138157 eval: func(args []Node, get lookup) Value {139- c := eval(args[0], get)158+ c := eval1(args[0], get)140159 if c.Kind == value.Error {141160 return c142161 }@@ -152,7 +171,7 @@ func init() {
152171 }153172 return eval(args[2], get)154173 }, format: func(args []Node, infer func(Node) Format) Format { return inherit(args[1:], infer) }},155- &FuncDef{Name: "IFERROR", Args: "value, [value_if_error]", Desc: "A fallback when a value is an error", Min: 1, Max: 2,174+ &FuncDef{Name: "IFERROR", Args: "value, [value_if_error]", Desc: "A fallback when a value is an error", Min: 1, Max: 2, arrays: liftPass,156175 eval: func(args []Node, get lookup) Value {157176 v := eval(args[0], get)158177 if v.Kind != value.Error {@@ -199,7 +218,7 @@ func each(args []Node, get lookup, fn func(v Value, direct bool) *Value) *Value
199218 }200219 201220 func eachOf(arg Node, get lookup, fn func(v Value, direct bool) *Value) *Value {202- switch arg := arg.(type) {221+ switch arg := get.refOf(arg).(type) {203222 case formula.Ref:204223 return fn(get.cell(arg.Sheet, arg.Addr), false)205224 case formula.Empty:@@ -212,7 +231,36 @@ func eachOf(arg Node, get lookup, fn func(v Value, direct bool) *Value) *Value {
212231 })213232 return e214233 }215- return fn(evalArray(arg, get), true)234+ v := wholeArg(arg, get)235+ if a := get.arrayOf(v); a != nil {236+ return eachEntry(a, fn)237+ }238+ if v.Kind == value.Array {239+ v = value.ErrValue // a LAMBDA240+ }241+ return fn(v, true)242+}243+244+// eachEntry calls fn with the entries of an array, as a range's cells:245+// those it stores, then the rest, all its fill, unless that is blank.246+func eachEntry(a *Array, fn func(v Value, direct bool) *Value) *Value {247+ for _, v := range a.V {248+ if v.Kind == value.Empty {249+ continue250+ }251+ if e := fn(v, false); e != nil {252+ return e253+ }254+ }255+ if a.Fill.Kind == value.Empty {256+ return nil257+ }258+ for range a.size() - len(a.V) {259+ if e := fn(a.Fill, false); e != nil {260+ return e261+ }262+ }263+ return nil216264 }217265 218266 // Add counts v into the aggregate, with SUM's rules: blanks are skipped,@@ -253,7 +301,7 @@ func aggregate(done func(Agg) Value) func([]Node, lookup) Value {
253301 return func(args []Node, get lookup) Value {254302 s := NewAgg()255303 for _, arg := range args {256- if rn, ok := arg.(formula.Range); ok {304+ if rn, ok := get.refOf(arg).(formula.Range); ok {257305 var e *Value258306 if s, e = rangeAgg(rn, s, get); e != nil {259307 return *e@@ -8,22 +8,12 @@ import (
88 // IsVolatile reports whether n calls a function whose result changes99 // without its inputs changing (TODAY, NOW, RAND).1010 func IsVolatile(n Node) bool {11- switch n := n.(type) {12- case formula.Unary:13- return IsVolatile(n.X)14- case formula.Binary:15- return IsVolatile(n.L) || IsVolatile(n.R)16- case formula.Call:17- if funcOf(n).Volatile {18- return true19- }20- for _, a := range n.Args {21- if IsVolatile(a) {22- return true23- }24- }11+ if c, ok := n.(formula.Call); ok && funcOf(c).Volatile {12+ return true2513 }26- return false14+ volatile := false15+ formula.EachChild(n, func(k Node) { volatile = volatile || IsVolatile(k) })16+ return volatile2717 }2818 2919 // InferFormat picks the format Sheets shows a formula's result in when@@ -58,6 +48,8 @@ func InferFormat(n Node, at func(string, Addr) Format) Format {
5848 if funcOf(n).format != nil {5949 return funcOf(n).format(n.Args, func(n Node) Format { return InferFormat(n, at) })6050 }51+ case formula.Array:52+ return InferFormat(n.Rows[0][0], at)6153 }6254 return Format{}6355 }@@ -82,7 +82,7 @@ func (e inputError) Error() string { return e.v.Str }
8282 // of more than jevWhole cells leaves off the blank rows and columns past8383 // its data, so a whole column is its data.8484 func jevState(n Node, get lookup) (any, error) {85- if rn, ok := n.(formula.Range); ok {85+ if rn, ok := get.refOf(n).(formula.Range); ok {8686 m := rectMatrix(rn.Sheet, rn.Rect, get)8787 if m.blank.Kind == value.Error {8888 return nil, inputError{m.blank}@@ -148,7 +148,7 @@ func jevQuestion(n Node, get lookup) (string, error) {
148148 // skipped.149149 func jevList(n Node, get lookup) ([]string, error) {150150 var out []string151- if rn, ok := n.(formula.Range); ok {151+ if rn, ok := get.refOf(n).(formula.Range); ok {152152 var err error153153 get.cells(rn.Sheet, rn.Rect, func(_ Addr, v Value) bool {154154 if v.Kind == value.Error {@@ -0,0 +1,124 @@
1+package functions2+3+import (4+ "github.com/FelineStateMachine/012/internal/formula"5+ "github.com/FelineStateMachine/012/internal/value"6+)7+8+// In an array context (ARRAYFORMULA, or an argument that takes ranges) a9+// function of one value given arrays is applied to each of their10+// entries, as Sheets does: ARRAYFORMULA(LEN(A1:A3)) is the length of11+// each cell, and ARRAYFORMULA(VLOOKUP(A1:A3, D:E, 2, FALSE)) looks each12+// one up. Which arguments are mapped over isn't listed anywhere: the13+// function is called once with its arguments standing in (liftArg), and14+// those it reads as one value are the ones mapped over, while those it15+// reads whole (VLOOKUP's table, SUMIF's range) stay whole. So16+// SUM(LEN(A1:A3)) adds three lengths, and SUMIF(A:A, D1:D3) inside17+// ARRAYFORMULA sums once per criterion.18+19+// liftFrame is one function call being mapped over arrays: its20+// arguments, their values in the array context once read, and the entry21+// the call is computing.22+type liftFrame struct {23+ args []Node24+ vals []Value25+ got []bool // vals[i] is computed26+ read []bool // argument i was read as one value, and is an array27+ r, c int // the entry being computed28+ fill bool // computing the value past every argument's data29+}30+31+// liftArg stands for argument i of a call being mapped over arrays.32+// Read as one value (eval), it is the entry being computed; read whole33+// (matrixArg, eachOf, arrayArg), it is the argument itself.34+type liftArg struct {35+ f *liftFrame36+ i int37+}38+39+// value is the argument in the array context: evaluated once.40+func (a liftArg) value(get lookup) Value {41+ f := a.f42+ if !f.got[a.i] {43+ f.vals[a.i], f.got[a.i] = evalArr(f.args[a.i], get), true44+ }45+ return f.vals[a.i]46+}47+48+// one is the argument's entry for the call being computed.49+func (a liftArg) one(get lookup) Value {50+ v := a.value(get)51+ arr := get.arrayOf(v)52+ if arr == nil {53+ return v54+ }55+ f := a.f56+ f.read[a.i] = true57+ if f.fill {58+ return arr.Fill59+ }60+ return arr.bcast(f.r, f.c)61+}62+63+// unlift is the node an argument stands for, when it is a reference or64+// range a function reads whole, or n itself.65+func unlift(n Node) Node {66+ if a, ok := n.(liftArg); ok {67+ switch orig := a.f.args[a.i].(type) {68+ case formula.Range, formula.Ref:69+ return orig70+ }71+ }72+ return n73+}74+75+// liftCall calls f with args in an array context, mapping it over the76+// arrays it reads as one value.77+func liftCall(f *FuncDef, args []Node, get lookup) Value {78+ fr := &liftFrame{args: args, vals: make([]Value, len(args)), got: make([]bool, len(args)), read: make([]bool, len(args))}79+ stand := make([]Node, len(args))80+ for i, a := range args {81+ switch a.(type) {82+ case formula.Num, formula.Str, formula.Bool, formula.Empty, formula.Ref:83+ stand[i] = a // one value however it's read84+ default:85+ stand[i] = liftArg{fr, i}86+ }87+ }88+ lift, want := get.lift, get.wantArr89+ get.lift, get.wantArr = 0, f.arrays == liftPass90+ defer func() { get.lift, get.wantArr = lift, want }()91+ v := get.first(f.call(stand, get))92+ var s shape93+ var arrs []*Array94+ for i, r := range fr.read {95+ if r {96+ a := get.arrayOf(fr.vals[i])97+ s.fit(a)98+ arrs = append(arrs, a)99+ }100+ }101+ if len(arrs) == 0 {102+ return v103+ }104+ s.data(arrs...)105+ out, ok := s.newArray()106+ if !ok {107+ return value.ErrValue108+ }109+ for i := range s.drows {110+ for j := range s.dcols {111+ if i == 0 && j == 0 { // the call above112+ out.Set(0, 0, v)113+ continue114+ }115+ fr.r, fr.c = i, j116+ out.Set(i, j, get.first(f.call(stand, get)))117+ }118+ }119+ if s.drows < s.rows || s.dcols < s.cols {120+ fr.fill = true121+ out.Fill = get.first(f.call(stand, get))122+ }123+ return get.arrayValue(out)124+}@@ -4,13 +4,13 @@ import "github.com/FelineStateMachine/012/internal/value"
44 55 func init() {66 define(7- &FuncDef{Name: "IFS", Args: "condition1, value1, [condition2, value2, ...]", Desc: "The value of the first true condition", Min: 2, Max: -1, step: 2,7+ &FuncDef{Name: "IFS", Args: "condition1, value1, [condition2, value2, ...]", Desc: "The value of the first true condition", Min: 2, Max: -1, step: 2, arrays: liftPass,88 eval: ifs, format: ifsFormat},9- &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,9+ &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, arrays: liftPass,1010 eval: switchCase},1111 &FuncDef{Name: "XOR", Args: "logical1, [logical2, ...]", Desc: "TRUE if an odd number of arguments are true", Min: 1, Max: -1,1212 eval: logical(func(t, _ int) bool { return t%2 == 1 })},13- &FuncDef{Name: "IFNA", Args: "value, value_if_na", Desc: "A fallback when a value is #N/A", Min: 2, Max: 2,13+ &FuncDef{Name: "IFNA", Args: "value, value_if_na", Desc: "A fallback when a value is #N/A", Min: 2, Max: 2, arrays: liftPass,1414 eval: ifNA, format: inherit},1515 &FuncDef{Name: "ISBLANK", Args: "value", Desc: "TRUE if a cell is empty", Min: 1, Max: 1,1616 eval: is(func(v Value) bool { return v.Kind == value.Empty })},@@ -53,13 +53,13 @@ func ifsFormat(args []Node, infer func(Node) Format) Format {
5353 }5454 5555 func switchCase(args []Node, get lookup) Value {56- x := eval(args[0], get)56+ x := eval1(args[0], get)5757 if x.Kind == value.Error {5858 return x5959 }6060 i := 16161 for ; i+1 < len(args); i += 2 {62- c := eval(args[i], get)62+ c := eval1(args[i], get)6363 if c.Kind == value.Error {6464 return c6565 }@@ -18,7 +18,7 @@ func init() {
1818 eval: index, format: inheritFrom(0)},1919 &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,2020 eval: xlookup, format: inheritFrom(2)},21- &FuncDef{Name: "CHOOSE", Args: "index, choice1, [choice2, ...]", Desc: "The choice at a position", Min: 2, Max: -1,21+ &FuncDef{Name: "CHOOSE", Args: "index, choice1, [choice2, ...]", Desc: "The choice at a position", Min: 2, Max: -1, arrays: liftPass,2222 eval: choose},2323 &FuncDef{Name: "ROWS", Args: "range", Desc: "Number of rows in a range", Min: 1, Max: 1,2424 eval: func(args []Node, get lookup) Value { return num(float64(matrixArg(args[0], get).rows)) }},@@ -82,14 +82,37 @@ func index(args []Node, get lookup) Value {
8282 col = 18383 }8484 switch {85- case row == 0 || col == 0:86- return value.ErrValue // a whole row or column: an array we can't show8785 case row > m.rows || col > m.cols:8886 return value.ErrRef87+ case row == 0 || col == 0: // a whole column, row or both: an array88+ return get.arrayValue(m.slice(row, col))8989 }9090 return m.cell(row-1, col-1)9191 }9292 93+// slice is row (0 for all) and col (0 for all) of m as an array.94+func (m matrix) slice(row, col int) *Array {95+ r0, c0, rows, cols := row-1, col-1, 1, 196+ if row == 0 {97+ r0, rows = 0, m.rows98+ }99+ if col == 0 {100+ c0, cols = 0, m.cols101+ }102+ a := &Array{Rows: rows, Cols: cols, Fill: m.blank,103+ DRows: min(rows, max(m.dataRows-r0, 0)), DCols: min(cols, max(m.dataCols-c0, 0))}104+ if tooBig(a.DRows, a.DCols) {105+ return &Array{Rows: 1, Cols: 1, Fill: value.ErrValue}106+ }107+ a.V = make([]Value, a.DRows*a.DCols)108+ for r := range a.DRows {109+ for c := range a.DCols {110+ a.Set(r, c, m.cell(r0+r, c0+c))111+ }112+ }113+ return a114+}115+93116 func choose(args []Node, get lookup) Value {94117 i, err := intArg(args, 0, 0, get)95118 if err != nil {@@ -52,8 +52,27 @@ type Reader struct {
5252 bufs []*scanBuf5353 level int // how many of bufs are in use by range reads in progress5454 55- here Addr // the cell whose formula is being evaluated; see EvalAt56- arrays int // how many arguments taking ranges enclose the operator being evaluated; see evalArray55+ evalState56+ // arena holds the arrays and LAMBDAs of the formulas being evaluated57+ // (array.go), emptied when the outermost is done; nest counts them.58+ arena []any59+ nest int60+}61+62+// evalState is where a formula's evaluation is: saved while a cell it63+// reads is evaluated through the same Reader, and given back after.64+type evalState struct {65+ here Addr // the cell whose formula is being evaluated; see EvalAt66+ // wantArr is set when the expression being evaluated may compute an67+ // array: at the top of a cell's formula, in operands and in what68+ // IF-like functions return. Elsewhere an array reads as its first69+ // entry (Reader.reduce).70+ wantArr bool71+ // lift counts the array contexts enclosing the expression: ranges72+ // read whole and functions of one value are mapped over arrays.73+ lift int74+ // scope holds the names LET and LAMBDA bound, innermost last.75+ scope []binding5776 }5877 5978 // lookup is the parameter every evaluator takes.@@ -0,0 +1,150 @@
1+package functions2+3+import (4+ "strings"5+6+ "github.com/FelineStateMachine/012/internal/formula"7+ "github.com/FelineStateMachine/012/internal/value"8+)9+10+// Names LET and LAMBDA bind (Local nodes), and calling a LAMBDA: the11+// scope a formula is evaluated in. The functions themselves are in12+// lambda.go.13+14+// binding is a name LET or LAMBDA bound: to a reference or range (ref),15+// which then reads as the range does wherever it's used, or to a value.16+type binding struct {17+ name string18+ ref Node19+ val Value20+}21+22+// lookupName finds the innermost binding of name.23+func (rd *Reader) lookupName(name string) (binding, bool) {24+ for i := len(rd.scope) - 1; i >= 0; i-- {25+ if strings.EqualFold(rd.scope[i].name, name) {26+ return rd.scope[i], true27+ }28+ }29+ return binding{}, false30+}31+32+// local is the value of a bound name.33+func (rd *Reader) local(name string) Value {34+ b, ok := rd.lookupName(name)35+ switch {36+ case !ok:37+ return value.ErrName38+ case b.ref != nil:39+ return eval(b.ref, rd)40+ }41+ return b.val42+}43+44+// refOf is the reference or range n stands for when a function reads it45+// whole: n itself, a name bound to one, or the argument an entry being46+// mapped over stands in for.47+func (rd *Reader) refOf(n Node) Node {48+ switch l := n.(type) {49+ case formula.Local:50+ if b, ok := rd.lookupName(l.Name); ok && b.ref != nil {51+ return b.ref52+ }53+ case liftArg:54+ return unlift(n)55+ }56+ return n57+}58+59+// bind evaluates what a name is bound to: a reference or range stays60+// one, read as it would be where the name is used, except in an array61+// context, which reads a range whole.62+func (rd *Reader) bind(name string, n Node) binding {63+ if rd.lift == 0 {64+ switch r := rd.refOf(n).(type) {65+ case formula.Ref, formula.Range:66+ return binding{name: name, ref: r}67+ }68+ }69+ want := rd.wantArr70+ rd.wantArr = true71+ v := eval(n, rd)72+ rd.wantArr = want73+ return binding{name: name, val: v}74+}75+76+// invoke evaluates a LAMBDA call written in the formula: LAMBDA(...)(...)77+// or a name bound to a LAMBDA with arguments.78+func (rd *Reader) invoke(n formula.Invoke) Value {79+ want := rd.wantArr80+ rd.wantArr = true81+ fn := eval(n.Fn, rd)82+ rd.wantArr = want83+ l := rd.lambdaOf(fn)84+ switch {85+ case fn.Kind == value.Error:86+ return fn87+ case l == nil:88+ return value.ErrValue89+ case len(n.Args) != len(l.params):90+ return value.ErrNA // Sheets: wrong number of arguments91+ }92+ vals := make([]binding, len(n.Args))93+ for i, a := range n.Args {94+ vals[i] = rd.bind(l.params[i], a)95+ }96+ return rd.apply(l, vals)97+}98+99+// apply evaluates a LAMBDA's expression with its parameters bound, in100+// the scope it was written in.101+func (rd *Reader) apply(l *lambda, args []binding) Value {102+ saved := rd.scope103+ rd.scope = append(append([]binding(nil), l.scope...), args...)104+ want := rd.wantArr105+ rd.wantArr = true106+ v := eval(l.body, rd)107+ rd.wantArr = want108+ rd.scope = saved109+ return v110+}111+112+// call calls a LAMBDA with values, for the functions that take one,113+// outside any array context: its expression sees the values it's given.114+func (rd *Reader) call(l *lambda, vals ...Value) Value {115+ args := make([]binding, len(vals))116+ for i, v := range vals {117+ args[i] = binding{name: l.params[i], val: v}118+ }119+ lift := rd.lift120+ rd.lift = 0121+ v := rd.apply(l, args)122+ rd.lift = lift123+ return v124+}125+126+// lambdaArg reads a LAMBDA argument taking n values: #N/A when it takes127+// another number, as Sheets says.128+func lambdaArg(n Node, get lookup, params int) (*lambda, *Value) {129+ v := wholeArg(n, get)130+ if v.Kind == value.Error {131+ return nil, errOf(v)132+ }133+ l := get.lambdaOf(v)134+ switch {135+ case l == nil:136+ return nil, &value.ErrValue137+ case len(l.params) != params:138+ return nil, &value.ErrNA139+ }140+ return l, nil141+}142+143+// single is what a LAMBDA called for one entry returns: an array there144+// is #VALUE!, as Sheets says a LAMBDA must return one value.145+func (rd *Reader) single(v Value) Value {146+ if v.Kind == value.Array {147+ return value.ErrValue148+ }149+ return v150+}@@ -21,7 +21,12 @@ type seq struct {
2121 // searches it: through the cells it holds, when it is a range.2222 func lineSeq(m matrix, get lookup, vertical bool) seq {2323 line := m24- if m.ref {24+ switch {25+ case !m.ref && vertical && m.cols > 1:26+ line = m.part(m.rows, 1)27+ case !m.ref && !vertical && m.rows > 1:28+ line = m.part(1, m.cols)29+ case m.ref:2530 to := Addr{Col: m.origin.Col, Row: m.origin.Row + m.rows - 1}2631 if !vertical {2732 to = Addr{Col: m.origin.Col + m.cols - 1, Row: m.origin.Row}@@ -80,7 +80,7 @@ func (e *evaluator) compute(s *Sheet, a Addr) Value {
8080 return ErrRef8181 case st != dirty:8282 return c.Value83- case c.derived: // a pivot's result, set when the pivot was computed83+ case c.derived || c.spilled: // a pivot's or an array's result, set when it was computed8484 s.calc[a] = done8585 return c.Value8686 case c.expr != nil && w.depth >= maxEvalDepth:@@ -117,7 +117,9 @@ func (e *evaluator) formula(s *Sheet, a Addr, c *Cell) {
117117 expr := s.bound(c)118118 outer := w.evaluating119119 w.evaluating = loc{s, a}120- c.Value = functions.EvalAt(w.arith(expr), s.calcGet.lib, a)120+ v, arr := functions.EvalCell(w.arith(expr), s.calcGet.lib, a)121+ c.Value = v122+ w.noteSpill(loc{s, a}, arr, v)121123 w.evaluating = outer122124 c.auto = functions.InferFormat(expr, s.calcFmt)123125 w.depth--@@ -35,6 +35,16 @@ func (s *Sheet) explain(a Addr, path []loc) string {
3535 if c != nil && c.derived && v.Kind == Error && s.pivot.err != "" {3636 return s.pivot.err3737 }38+ if why := s.spillError(a); why != "" && v == ErrRef {39+ return why40+ }41+ if anchor, ok := s.SpillAnchor(a); ok && v.Kind == Error {42+ why := s.explain(anchor, path)43+ if why == "" {44+ return "Spilled from " + anchor.String()45+ }46+ return "Spilled from " + anchor.String() + ": " + strings.ToLower(why[:1]) + why[1:]47+ }3848 if c == nil || v.Kind != Error || !c.IsFormula() {3949 return ""4050 }@@ -303,14 +303,14 @@ func (s *Sheet) writeBody(b *bytes.Buffer, indent, names string) error {
303303 }304304 addrs := make([]Addr, 0, s.cells.len())305305 for a, c := range s.cells.all() {306- if !c.derived { // a pivot's results are computed, not saved306+ if c.saved() != nil {307307 addrs = append(addrs, a)308308 }309309 }310310 sortAddrs(addrs)311311 b.WriteString(indent + `"cells": {`)312312 for i, a := range addrs {313- raw, err := encodeCell(s.cells.get(a))313+ raw, err := encodeCell(s.cells.get(a).saved())314314 if err != nil {315315 return err316316 }@@ -86,7 +86,7 @@ func (s *Sheet) Replace(a Addr, query, repl string, o FindOptions) (bool, error)
8686 8787 func (s *Sheet) replace(a Addr, f *finder, repl string) (bool, error) {8888 c := s.cells.get(a)89- if c == nil || c.derived || (c.IsFormula() && !f.o.InFormulas) {89+ if c == nil || c.derived || c.spilled || (c.IsFormula() && !f.o.InFormulas) {9090 return false, nil9191 }9292 old := f.text(c)@@ -0,0 +1,120 @@
1+package sheet2+3+import (4+ "strings"5+ "testing"6+)7+8+// arrCase is a formula and the grid it shows from its cell, rows joined9+// with | between cells: one row for a single value.10+type arrCase struct {11+ in string12+ want []string13+}14+15+// checkArrays enters each formula in K10, below and right of the16+// fixture's data, and compares what it spills.17+func checkArrays(t *testing.T, s *Sheet, tests []arrCase) {18+ t.Helper()19+ anchor := at("K10")20+ for _, tt := range tests {21+ if err := s.Set(anchor, tt.in); err != nil {22+ t.Errorf("Set(%q): %v", tt.in, err)23+ continue24+ }25+ area, ok := s.SpillArea(anchor)26+ if !ok {27+ area = Rect{From: anchor, To: anchor}28+ }29+ var got []string30+ for r := area.From.Row; r <= area.To.Row; r++ {31+ var row []string32+ for c := area.From.Col; c <= area.To.Col; c++ {33+ row = append(row, s.Value(Addr{Col: c, Row: r}).String())34+ }35+ got = append(got, strings.Join(row, "|"))36+ }37+ if strings.Join(got, "\n") != strings.Join(tt.want, "\n") {38+ t.Errorf("%s spills\n %s\nwant\n %s", tt.in, strings.Join(got, "\n "), strings.Join(tt.want, "\n "))39+ }40+ }41+ s.Set(anchor, "")42+}43+44+func TestArrayOperators(t *testing.T) {45+ checkArrays(t, fixture(t), []arrCase{46+ {"={1,2;3,4}", []string{"1|2", "3|4"}},47+ {"={1,2}*10", []string{"10|20"}},48+ {"={1;2}+{10,20}", []string{"11|21", "12|22"}},49+ {"={1,2,3}+{1,2}", []string{"2|4|#N/A"}},50+ {`={"a","b"}&"!"`, []string{"a!|b!"}},51+ {"=-{1,2}", []string{"-1|-2"}},52+ {"={1,2}/0", []string{"#DIV/0!|#DIV/0!"}},53+ {"={C1:C2,G1:G2}", []string{"1|1", "2|2"}},54+ {"={C1:C2;G1}", []string{"1", "2", "1"}},55+ {"={C1:C2,G1}", []string{"#VALUE!"}},56+ {"={1,2;3}", []string{"#VALUE!"}},57+ {"=SUM(C1:C3*2)", []string{"12"}},58+ {"=SUMPRODUCT(C1:C3*G1:G3)", []string{"11"}},59+ {"=SUMPRODUCT((B1:B5=\"north\")*C1:C5)", []string{"9"}},60+ {"=SUM({1,2,3})", []string{"6"}},61+ {"=COUNT(C:C*1)", []string{"1048576"}},62+ {"=MAX(LEN(D1:D5))", []string{"6"}},63+ {"=C1:C3*2", []string{"#VALUE!"}}, // row 10: no intersection64+ {"=ARRAYFORMULA(C1:C3*2)", []string{"2", "4", "6"}},65+ {"=ARRAYFORMULA(LEN(D1:D3))", []string{"5", "6", "6"}},66+ {`=ARRAYFORMULA(IF(C1:C4>2, "big", "small"))`, []string{"small", "small", "big", "big"}},67+ {"=ARRAYFORMULA(VLOOKUP(G1:G3, C1:D5, 2, FALSE))", []string{"Apple", "Banana", "Banana"}},68+ {`=ARRAYFORMULA(SUMIF(B1:B5, {"north";"east"}, C1:C5))`, []string{"9", "4"}},69+ {"=ARRAYFORMULA(A1:A3)", []string{"10", "20", "30"}},70+ {"=ARRAYFORMULA(ROUND(E1:E3))", []string{"2", "3", "4"}},71+ {"=LEN(SEQUENCE(3)*100)", []string{"3"}},72+ {"=INDEX(C1:D3, 0, 2)", []string{"Apple", "Banana", "Cherry"}},73+ {"=INDEX(C1:D3, 2, 0)", []string{"2|Banana"}},74+ {"=INDEX({1,2;3,4}, 2, 1)", []string{"3"}},75+ {"=ROWS(SEQUENCE(4))", []string{"4"}},76+ {`=IFERROR(FILTER(C1:C5, C1:C5>9), "none")`, []string{"none"}},77+ {"=IF(TRUE, SEQUENCE(2), 0)", []string{"1", "2"}},78+ {"=MATCH(3, SEQUENCE(5), 0)", []string{"3"}},79+ {"=VLOOKUP(2, {1,\"a\";2,\"b\"}, 2, FALSE)", []string{"b"}},80+ })81+}82+83+func TestArrayFunctions(t *testing.T) {84+ checkArrays(t, fixture(t), []arrCase{85+ {"=SEQUENCE(3)", []string{"1", "2", "3"}},86+ {"=SEQUENCE(2, 3)", []string{"1|2|3", "4|5|6"}},87+ {"=SEQUENCE(2, 2, 10, -5)", []string{"10|5", "0|-5"}},88+ {"=SEQUENCE(0)", []string{"#VALUE!"}},89+ {"=FILTER(D1:D5, C1:C5>3)", []string{"date", "Apple"}},90+ {`=FILTER(C1:D5, B1:B5="north", C1:C5>1)`, []string{"3|Cherry", "5|Apple"}},91+ {"=FILTER(C1:E1, {TRUE,FALSE,TRUE})", []string{"1|1.5"}},92+ {"=FILTER(C1:C5, C1:C5>10)", []string{"#N/A"}},93+ {"=FILTER(C1:C5, C1:C4>1)", []string{"#VALUE!"}},94+ {"=FILTER(C:C, C:C>5)", []string{"6", "7"}},95+ {"=SORT(I1:I3)", []string{"10", "20", "30"}},96+ {"=SORT(C1:D3, 2, FALSE)", []string{"3|Cherry", "2|Banana", "1|Apple"}},97+ {"=SORT(D1:D5, 1, TRUE)", []string{"Apple", "Apple", "Banana", "Cherry", "date"}},98+ {"=SORT(C1:C3, G1:G3, FALSE)", []string{"2", "3", "1"}},99+ {"=SORT(A1:A7)", []string{"-5", "10", "20", "30", "apple", "TRUE", ""}},100+ {"=SORTN(C1:C7, 2, 0, 1, FALSE)", []string{"7", "6"}},101+ {"=SORTN(G1:G4, 2, 1, 1, TRUE)", []string{"1", "2", "2"}},102+ {"=SORTN(G1:G4, 2, 2)", []string{"1", "2"}},103+ {"=SORTN(G1:G4, 2, 3)", []string{"1", "2", "2"}},104+ {"=SORTN(C1:C3)", []string{"1"}},105+ {"=UNIQUE(D1:D5)", []string{"Apple", "Banana", "Cherry", "date"}},106+ {"=UNIQUE(G1:G4)", []string{"1", "2", "3"}},107+ {"=UNIQUE(G1:G4, FALSE, TRUE)", []string{"1", "3"}},108+ {`=UNIQUE({"a","a","A"}, TRUE)`, []string{"a|A"}},109+ {"=UNIQUE(B:B)", []string{"north", "south", "east", "", "Ab*d"}},110+ {"=TRANSPOSE(C1:D2)", []string{"1|2", "Apple|Banana"}},111+ {"=FLATTEN(C1:D2)", []string{"1", "Apple", "2", "Banana"}},112+ {"=FLATTEN(C1:C2, G1:G2)", []string{"1", "2", "1", "2"}},113+ {"=CHOOSECOLS(C1:E2, 3, -3)", []string{"1.5|1", "2.5|2"}},114+ {"=CHOOSEROWS(C1:D3, -1, 1)", []string{"3|Cherry", "1|Apple"}},115+ {"=CHOOSEROWS(C1:D3, 4)", []string{"#VALUE!"}},116+ {"=COUNTA(UNIQUE(B1:B5))", []string{"3"}},117+ {`=TEXTJOIN(",", TRUE, FILTER(D1:D5, C1:C5<3))`, []string{"Apple,Banana"}},118+ {`=TEXTJOIN("-", TRUE, SEQUENCE(3))`, []string{"1-2-3"}},119+ })120+}@@ -27,7 +27,7 @@ func TestLookupFunctions(t *testing.T) {
2727 {"=INDEX(D1:D3, 3)", txt("Cherry")},2828 {"=INDEX(C1:E1, 2)", txt("Apple")}, // a single row counts across2929 {"=INDEX(D1:E3, 4, 1)", ErrRef},30- {"=INDEX(D1:E3, 2)", ErrValue}, // a whole row30+ {"=INDEX(D1:E3, 2)", txt("Banana")}, // a whole row, spilled3131 {`=XLOOKUP("Cherry", D1:D3, E1:E3)`, num(3.5)},3232 {`=XLOOKUP("kiwi", D1:D3, E1:E3)`, ErrNA},3333 {`=XLOOKUP("kiwi", D1:D3, E1:E3, "none")`, txt("none")},@@ -447,6 +447,8 @@ func (w *Workbook) swap(undo bool) (Change, bool) {
447447 s.view = *v448448 s.hidden.valid = false449449 }450+ changed = append(changed, h.dirty...) // anchors of spills the step touched451+ h.dirty = nil450452 if w.structural {451453 w.structural = false452454 w.recalcAll()@@ -29,7 +29,7 @@ func TestImplicitIntersection(t *testing.T) {
2929 "F2": "2800", "F3": "2900", "F4": "3000", "F5": "#VALUE!",3030 "G3": "1450", "G4": "1500", "G5": "1",3131 "H3": "4350", "H4": "4350", "H2": "2",32- "H5": "#VALUE!", "H6": "#VALUE!", // array arguments: not intersected32+ "H5": "8700", "H6": "8700", // array arguments: computed over the whole range3333 "C7": "21", "E7": "#VALUE!", "A8": "#VALUE!",3434 "J3": "1450", "K3": "1450!", "L3": "2",3535 } {@@ -442,7 +442,7 @@ func derivedInput(v Value) string {
442442 // plain returns a copy of c as an ordinary cell: a pivot's result becomes443443 // the constant it shows, so copying results pastes values.444444 func (c *Cell) plain() *Cell {445- if c == nil || !c.derived {445+ if c == nil || !c.derived && !c.spilled {446446 return c.clone()447447 }448448 p, err := newCell(valueInput(c.Value), c.Format, c.Style, false)@@ -36,6 +36,7 @@ func (w *Workbook) recalcAll() {
3636 n += len(s.calc)3737 }3838 w.evaluate()39+ w.settleSpills(nil)3940 w.refreshPivots(w.allPivots())4041 w.observe(true, start, n)4142 }@@ -60,7 +61,7 @@ func (w *Workbook) recalcFrom(changed []loc, volatiles bool) {
6061 n := w.affected(changed, volatiles)6162 stale := w.stalePivots()6263 w.evaluate()63- w.refreshPivots(stale)64+ w.refreshPivots(w.settleSpills(stale))6465 w.observe(false, start, n)6566 }6667 @@ -68,6 +69,13 @@ func (w *Workbook) recalcFrom(changed []loc, volatiles bool) {
6869 // formulas if volatiles is set, and every formula that transitively reads6970 // them, on any sheet, and returns how many it marked.7071 func (w *Workbook) affected(changed []loc, volatiles bool) int {72+ return w.affectedBy(changed, nil, volatiles)73+}74+75+// affectedBy is affected, with the formulas reading the cells of76+// readers marked too, but not those cells: spilling changed what an77+// anchor shows (spill.go), not its formula.78+func (w *Workbook) affectedBy(changed, readers []loc, volatiles bool) int {7179 m := marking{w: w, queue: append([]loc(nil), changed...), named: w.namedInUse(), byName: map[*Sheet]bool{}}7280 for _, s := range w.sheets {7381 s.calc = make(map[Addr]int)@@ -79,6 +87,9 @@ func (w *Workbook) affected(changed []loc, volatiles bool) int {
7987 m.queue = append(m.queue, loc{s, a})8088 }8189 }90+ for _, l := range readers {91+ m.readersOf(l)92+ }8293 n := 08394 for len(m.queue) > 0 {8495 l := m.queue[0]@@ -39,8 +39,13 @@ type Cell struct {
3939 names []string // names in expr, as keys of Workbook.names4040 volatile bool // expr calls TODAY, NOW, RAND...4141 derived bool // a pivot table's result, owned by the engine: see pivot.go42+ spilled bool // part of an array a formula spills, owned by it: see spill.go4243 }4344 45+// Spilled reports whether the cell shows part of an array another46+// cell's formula computed (see SpillAnchor).47+func (c *Cell) Spilled() bool { return c != nil && c.spilled }48+4449 // IsFormula reports whether the cell holds a formula.4550 func (c *Cell) IsFormula() bool {4651 return c.expr != nil && IsFormulaEntry(c.Input)@@ -87,6 +92,11 @@ type Sheet struct {
8792 charts []Chart // floating charts, bottom first; see chart.go8893 pivot pivotState // the sheet's pivot table, if any; see pivot.go8994 95+ // spills are the arrays formulas spill, by the anchor's address, and96+ // spillAt the anchors by the cells they cover; see spill.go.97+ spills map[Addr]*spill98+ spillAt rangeIndex99+90100 view viewState // frozen panes and the filter, see view.go91101 hidden hiddenCache // rows the filter hides, see filter.go92102 }@@ -216,6 +226,9 @@ func (s *Sheet) Set(a Addr, input string) error {
216226 if s.InPivot(Rect{From: a, To: a}) {217227 return ErrPivotEdit218228 }229+ if _, ok := s.SpillAnchor(a); ok {230+ return ErrSpillEdit231+ }219232 var err error220233 s.change("edit "+a.String(), Rect{From: a, To: a}, func() { err = s.put(a, input) })221234 return err@@ -306,14 +319,21 @@ func (c *Cell) setExpr(n Node) {
306319 307320 // place stores c at a (nil blanks it), recording the old cell for undo and308321 // keeping the dependency indexes current. Every cell mutation goes through309-// here, except a pivot table writing its results (setDerived). Anything310-// placed over those has the pivot recomputed, which reports it in the way.322+// here, except a pivot table or a spill writing its results (setDerived).323+// Anything placed over those has the pivot or the spill's formula324+// recomputed, which reports it in the way. A spilled cell placed (by undo,325+// a move or a format) places only its formatting: its value is its326+// anchor's to write.311327 func (s *Sheet) place(a Addr, c *Cell) {312328 s.version++313329 s.record(a)314330 if s.pivot.def != nil && (s.pivot.out.Contains(a) || s.pivot.err != "" && s.pivot.blocked.Contains(a)) {315331 s.pivot.stale = true316332 }333+ if c.Spilled() {334+ c = formattingOnly(c.Format, c.Style)335+ }336+ s.spillTouched(a, c)317337 if c == nil {318338 s.unlink(a)319339 return@@ -0,0 +1,332 @@
1+package sheet2+3+import (4+ "cmp"5+ "errors"6+ "fmt"7+ "maps"8+ "slices"9+10+ "github.com/FelineStateMachine/012/internal/functions"11+)12+13+// Spills, as Sheets' array results: a formula computing an array (its14+// anchor) shows the first value and spills the rest into the cells to15+// its right and below. Those are spilled cells, derived like a pivot's16+// results: owned by the anchor, rewritten outside the undo history17+// whenever it recomputes, refused by Set, constants to formulas reading18+// them, values to copies and exports, and never saved (the file keeps19+// the anchor's formula). Formatting a spilled cell keeps the formatting20+// and the anchor spills its value into it again.21+//22+// Spills are written after each evaluation pass: the formulas reading23+// spilled cells that changed are then recalculated, which may spill24+// again (a FILTER of a SORT), up to maxSpillPasses. When the cells an25+// array needs aren't empty, the anchor shows #REF! and says which cell26+// is in the way; changing that cell has the anchor try again.27+28+// spill is what an anchor spills, or would.29+type spill struct {30+ area Rect // the cells it covers, the anchor first; when blocked, those it needs31+ why string // why the anchor shows #REF! instead, or ""32+}33+34+// ErrSpillEdit is what typing into a spilled cell says: its value35+// belongs to the formula that spilled it.36+var ErrSpillEdit = errors.New("That cell holds part of an array result: edit the formula it spills from")37+38+// maxSpillPasses bounds the passes that spill arrays and recalculate what39+// reads them, so arrays reading each other can't loop.40+const maxSpillPasses = 6441+42+// spillWork is the arrays computed in an evaluation pass, by anchor,43+// with the value each anchor's formula computed.44+type spillWork map[loc]pendingSpill45+46+type pendingSpill struct {47+ arr *functions.Array // nil when the formula computed one value48+ v Value // what the formula computed, before spilling49+}50+51+// noteSpill records what the formula at l computed, when it spills or52+// did.53+func (w *Workbook) noteSpill(l loc, arr *functions.Array, v Value) {54+ if arr == nil && l.s.spills[l.a] == nil {55+ return56+ }57+ if w.spillWork == nil {58+ w.spillWork = spillWork{}59+ }60+ w.spillWork[l] = pendingSpill{arr, v}61+}62+63+// settleSpills writes the arrays evaluated, then recalculates what reads64+// the cells that changed, until nothing spills anew. It returns the65+// sheets whose pivots to refresh: stale, and those whose source a spill66+// changed.67+func (w *Workbook) settleSpills(stale []*Sheet) []*Sheet {68+ for pass := 0; len(w.spillWork) > 0; pass++ {69+ var changed, anchors []loc70+ for _, l := range w.spillOrder() {71+ cells, moved := l.s.applySpill(l.a, w.spillWork[l])72+ changed = append(changed, cells...)73+ if moved {74+ anchors = append(anchors, l)75+ }76+ }77+ w.spillWork = nil78+ if len(changed) == 0 && len(anchors) == 0 {79+ break80+ }81+ if pass == maxSpillPasses {82+ w.Circular = true83+ break84+ }85+ w.affectedBy(changed, anchors, false)86+ for _, s := range w.stalePivots() {87+ if !slices.Contains(stale, s) {88+ stale = append(stale, s)89+ }90+ }91+ w.evaluate()92+ }93+ return stale94+}95+96+// spillOrder is the anchors of spillWork in the order they spill: by97+// sheet, then row by row, so when two arrays need the same cells, the98+// first one written in that order gets them.99+func (w *Workbook) spillOrder() []loc {100+ order := slices.Collect(maps.Keys(w.spillWork))101+ slices.SortFunc(order, func(x, y loc) int {102+ return cmp.Or(cmp.Compare(w.Index(x.s), w.Index(y.s)), cmp.Compare(x.a.Row, y.a.Row), cmp.Compare(x.a.Col, y.a.Col))103+ })104+ return order105+}106+107+// applySpill writes what the anchor at a computed, returning the cells108+// that changed and whether the anchor's own value did (it shows #REF!109+// when blocked).110+func (s *Sheet) applySpill(a Addr, p pendingSpill) ([]loc, bool) {111+ c := s.cells.get(a)112+ if c == nil || !c.IsFormula() || p.arr == nil {113+ return s.dropSpill(a), false114+ }115+ area, why := s.spillArea(a, p.arr)116+ if why != "" {117+ changed := s.dropSpill(a)118+ s.setSpill(a, &spill{area: area, why: why})119+ c.Value = ErrRef120+ return changed, p.v != ErrRef121+ }122+ var changed []loc123+ if old := s.spills[a]; old != nil && old.why == "" {124+ changed = s.clearSpilled(old.area, area)125+ }126+ s.setSpill(a, &spill{area: area})127+ for r := range area.To.Row - a.Row + 1 {128+ for col := range area.To.Col - a.Col + 1 {129+ if r == 0 && col == 0 {130+ continue131+ }132+ at := Addr{Col: a.Col + col, Row: a.Row + r}133+ if s.writeSpilled(at, p.arr.At(r, col), c.auto) {134+ changed = append(changed, loc{s, at})135+ }136+ }137+ }138+ return changed, false139+}140+141+// spillArea is where the array computed at a goes, or why it can't: past142+// the sheet's edge, past max-cells, or over a cell that isn't empty.143+// Blank entries past the array's data aren't spilled, so a whole column144+// read as an array spills what it holds.145+func (s *Sheet) spillArea(a Addr, arr *functions.Array) (Rect, string) {146+ rows, cols := arr.Rows, arr.Cols147+ if arr.Fill.Kind == Empty {148+ rows, cols = min(rows, max(arr.DRows, 1)), min(cols, max(arr.DCols, 1))149+ }150+ area := Rect{From: a, To: Addr{Col: a.Col + cols - 1, Row: a.Row + rows - 1}}151+ switch {152+ case area.To.Row >= MaxRows || area.To.Col >= MaxCols:153+ area.To = Addr{Col: min(area.To.Col, MaxCols-1), Row: min(area.To.Row, MaxRows-1)}154+ return area, "Array result was not expanded because it would go past the edge of the sheet"155+ case rows*cols > MaxCells():156+ return area, fmt.Sprintf("Array result was not expanded because it would write more cells than max-cells allows (%d)", MaxCells())157+ }158+ old := s.spills[a]159+ for at, c := range s.cells.anyInRange(area) {160+ if at == a || c.Blank() || c.spilled && old != nil && old.why == "" && old.area.Contains(at) {161+ continue162+ }163+ return area, "Array result was not expanded because it would overwrite data in " + at.String()164+ }165+ return area, ""166+}167+168+// writeSpilled makes the cell at a show v as a spilled cell, keeping its169+// formatting, and reports whether it changed.170+func (s *Sheet) writeSpilled(a Addr, v Value, auto Format) bool {171+ old := s.cells.get(a)172+ var f Format173+ var st Style174+ if old != nil {175+ f, st = old.Format, old.Style176+ }177+ if v.Kind == Empty {178+ if old == nil || !old.spilled {179+ return false180+ }181+ s.setDerived(a, formattingOnly(f, st))182+ return true183+ }184+ c := &Cell{Input: derivedInput(v), Value: v, Format: f, Style: st, auto: auto, spilled: true}185+ if old != nil && old.spilled && old.Value == c.Value && old.Input == c.Input && old.auto == c.auto {186+ return false187+ }188+ s.setDerived(a, c)189+ return true190+}191+192+// clearSpilled turns the spilled cells of area outside keep back into193+// what they were before: blank, or formatting only.194+func (s *Sheet) clearSpilled(area, keep Rect) []loc {195+ var changed []loc196+ var gone []Addr197+ for at, c := range s.cells.anyInRange(area) {198+ if c.spilled && !keep.Contains(at) {199+ gone = append(gone, at)200+ }201+ }202+ for _, at := range gone {203+ c := s.cells.get(at)204+ s.setDerived(at, formattingOnly(c.Format, c.Style))205+ changed = append(changed, loc{s, at})206+ }207+ return changed208+}209+210+// dropSpill forgets the anchor at a's spill and clears its cells,211+// returning those that changed.212+func (s *Sheet) dropSpill(a Addr) []loc {213+ old := s.spills[a]214+ if old == nil {215+ return nil216+ }217+ s.setSpill(a, nil)218+ if old.why != "" {219+ return nil220+ }221+ return s.clearSpilled(old.area, Rect{From: a, To: a})222+}223+224+// setSpill records the anchor at a's spill (nil for none), indexing225+// its area so that changing a cell there recalculates the anchor.226+func (s *Sheet) setSpill(a Addr, sp *spill) {227+ if old := s.spills[a]; old != nil {228+ s.spillAt.remove(a, []Rect{old.area})229+ delete(s.spills, a)230+ }231+ if sp == nil {232+ return233+ }234+ if s.spills == nil {235+ s.spills = map[Addr]*spill{}236+ }237+ s.spills[a] = sp238+ s.spillAt.add(a, []Rect{sp.area})239+}240+241+// spillTouched notes that the cell at a is being changed by hand (or by242+// a paste, a sort or undo): the anchors whose spill covers or needs it243+// recompute, and an anchor losing its formula drops its spill.244+func (s *Sheet) spillTouched(a Addr, c *Cell) {245+ if s.spills == nil {246+ return247+ }248+ if s.spills[a] != nil && (c == nil || !c.IsFormula()) {249+ for _, l := range s.dropSpill(a) {250+ s.wb.markDirty(l)251+ }252+ }253+ s.spillAt.readers(a, func(anchor Addr) {254+ if anchor != a {255+ s.wb.markDirty(loc{s, anchor})256+ }257+ })258+}259+260+// markDirty has the step being made or undone recalculate the cell at261+// l, without recording it for undo.262+func (w *Workbook) markDirty(l loc) {263+ w.hist.dirty = append(w.hist.dirty, l)264+}265+266+// SpillAnchor returns the cell whose array result the cell at a shows,267+// when a is a spilled cell (not the anchor itself).268+func (s *Sheet) SpillAnchor(a Addr) (Addr, bool) {269+ var found Addr270+ ok := false271+ s.spillAt.readers(a, func(anchor Addr) {272+ if sp := s.spills[anchor]; anchor != a && sp.why == "" {273+ found, ok = anchor, true274+ }275+ })276+ return found, ok277+}278+279+// SpillArea returns the cells the formula at a spills into, the anchor280+// first, when it spills.281+func (s *Sheet) SpillArea(a Addr) (Rect, bool) {282+ sp := s.spills[a]283+ if sp == nil || sp.why != "" {284+ return Rect{}, false285+ }286+ return sp.area, true287+}288+289+// InSpill returns a spilled cell in r whose anchor isn't in r, which an290+// edit of r can't change: editing spilled cells is refused, while291+// clearing or replacing the anchor with its spill is fine.292+func (s *Sheet) InSpill(r Rect) (Addr, bool) {293+ for anchor, sp := range s.spills {294+ if sp.why != "" || r.Contains(anchor) || sp.area == (Rect{From: anchor, To: anchor}) {295+ continue296+ }297+ if overlap, ok := intersectRect(sp.area, r); ok {298+ return overlap.From, true // r doesn't hold the anchor: spilled cells299+ }300+ }301+ return Addr{}, false302+}303+304+// intersectRect is the overlap of two ranges, if any.305+func intersectRect(a, b Rect) (Rect, bool) {306+ r := Rect{307+ From: Addr{Col: max(a.From.Col, b.From.Col), Row: max(a.From.Row, b.From.Row)},308+ To: Addr{Col: min(a.To.Col, b.To.Col), Row: min(a.To.Row, b.To.Row)},309+ }310+ return r, r.From.Col <= r.To.Col && r.From.Row <= r.To.Row311+}312+313+// saved is what the file keeps of the cell, or nil: a pivot's results314+// are computed, not saved, and a spilled cell keeps its formatting, not315+// the array's value.316+func (c *Cell) saved() *Cell {317+ switch {318+ case c.derived:319+ return nil320+ case c.spilled:321+ return formattingOnly(c.Format, c.Style)322+ }323+ return c324+}325+326+// spillError says why the anchor at a shows #REF!, or "".327+func (s *Sheet) spillError(a Addr) string {328+ if sp := s.spills[a]; sp != nil {329+ return sp.why330+ }331+ return ""332+}@@ -0,0 +1,158 @@
1+package sheet2+3+import (4+ "bytes"5+ "strings"6+ "testing"7+)8+9+func wantShown(t *testing.T, s *Sheet, want map[string]string) {10+ t.Helper()11+ for a, w := range want {12+ if got := s.Value(at(a)).String(); got != w {13+ t.Errorf("%s = %q, want %q", a, got, w)14+ }15+ }16+}17+18+func TestSpillSequence(t *testing.T) {19+ s := sheetOf(t, map[string]string{"A1": "3", "B2": "=SEQUENCE(A1, 2)"})20+ wantShown(t, s, map[string]string{"B2": "1", "C2": "2", "B3": "3", "C3": "4", "B4": "5", "C4": "6", "B5": ""})21+ if a, ok := s.SpillAnchor(at("C3")); !ok || a != at("B2") {22+ t.Errorf("C3's anchor = %v %v, want B2", a, ok)23+ }24+ if _, ok := s.SpillAnchor(at("B2")); ok {25+ t.Error("the anchor is its own cell, not spilled")26+ }27+ if r, ok := s.SpillArea(at("B2")); !ok || r != rect("B2:C4") {28+ t.Errorf("area %v %v, want B2:C4", r, ok)29+ }30+ // Growing and shrinking rewrite the spilled cells.31+ s.Set(at("A1"), "4")32+ wantShown(t, s, map[string]string{"B5": "7", "C5": "8"})33+ s.Set(at("A1"), "1")34+ wantShown(t, s, map[string]string{"B2": "1", "C2": "2", "B3": "", "C5": ""})35+ if s.Cell(at("B3")) != nil {36+ t.Error("a cell the spill left is still stored")37+ }38+ // Undo brings the bigger array back.39+ s.Book().Undo()40+ wantShown(t, s, map[string]string{"B5": "7", "C5": "8"})41+}42+43+func TestSpillBlocked(t *testing.T) {44+ s := sheetOf(t, map[string]string{"A1": "=SEQUENCE(3)", "A3": "note"})45+ wantShown(t, s, map[string]string{"A1": "#REF!", "A2": "", "A3": "note"})46+ if got := s.ExplainError(at("A1")); got != "Array result was not expanded because it would overwrite data in A3" {47+ t.Errorf("A1 explained as %q", got)48+ }49+ // Clearing what's in the way lets it spill.50+ if err := s.Set(at("A3"), ""); err != nil {51+ t.Fatal(err)52+ }53+ wantShown(t, s, map[string]string{"A1": "1", "A2": "2", "A3": "3"})54+ // Spilled cells can't be typed into.55+ if err := s.Set(at("A2"), "x"); err != ErrSpillEdit {56+ t.Errorf("Set in a spill: %v, want ErrSpillEdit", err)57+ }58+ if a, ok := s.InSpill(rect("A2:B9")); !ok || a != at("A2") {59+ t.Errorf("InSpill(A2:B9) = %v %v", a, ok)60+ }61+ if _, ok := s.InSpill(rect("A1:A3")); ok {62+ t.Error("a range holding the anchor can be edited as a whole")63+ }64+ // The sheet's edge blocks too.65+ s.Set(at("A1048575"), "=SEQUENCE(5)")66+ if got := s.ExplainError(at("A1048575")); !strings.Contains(got, "edge of the sheet") {67+ t.Errorf("at the edge: %q", got)68+ }69+}70+71+func TestSpillDependents(t *testing.T) {72+ w := NewBook()73+ s := w.Sheet(0)74+ other, err := w.AddSheet("Other", 1)75+ if err != nil {76+ t.Fatal(err)77+ }78+ for a, in := range map[string]string{"A1": "5", "A2": "3", "A3": "8", "C1": "=SORT(A1:A3)", "E1": "=C3*10", "E2": "=SUM(C1:C9)"} {79+ if err := s.Set(at(a), in); err != nil {80+ t.Fatal(err)81+ }82+ }83+ if err := other.Set(at("A1"), "=Sheet1!C2+1"); err != nil {84+ t.Fatal(err)85+ }86+ wantShown(t, s, map[string]string{"C1": "3", "C2": "5", "C3": "8", "E1": "80", "E2": "16"})87+ wantShown(t, other, map[string]string{"A1": "6"})88+ s.Set(at("A2"), "9")89+ wantShown(t, s, map[string]string{"C1": "5", "C2": "8", "C3": "9", "E1": "90", "E2": "22"})90+ wantShown(t, other, map[string]string{"A1": "9"})91+ // A spill reading a spill.92+ s.Set(at("G1"), "=FILTER(C1:C3, C1:C3>5)")93+ wantShown(t, s, map[string]string{"G1": "8", "G2": "9", "G3": ""})94+ s.Set(at("A1"), "1")95+ wantShown(t, s, map[string]string{"C1": "1", "G1": "8", "G2": "9"})96+ // Deleting the anchor clears its spill, and what read it follows.97+ s.Set(at("C1"), "")98+ wantShown(t, s, map[string]string{"C2": "", "C3": "", "E1": "0", "E2": "0", "G1": "#N/A"})99+ wantShown(t, other, map[string]string{"A1": "1"})100+ w.Undo()101+ wantShown(t, s, map[string]string{"C3": "9", "E1": "90", "G1": "8"})102+}103+104+func TestSpillNotSaved(t *testing.T) {105+ s := sheetOf(t, map[string]string{"A1": "=SEQUENCE(2)", "B1": "x"})106+ var buf bytes.Buffer107+ if err := s.Book().Write(&buf); err != nil {108+ t.Fatal(err)109+ }110+ if strings.Contains(buf.String(), `"A2"`) {111+ t.Errorf("spilled cell saved:\n%s", buf.String())112+ }113+ w, err := ReadBook(&buf)114+ if err != nil {115+ t.Fatal(err)116+ }117+ wantShown(t, w.Sheet(0), map[string]string{"A1": "1", "A2": "2"})118+}119+120+func TestSpillStructure(t *testing.T) {121+ s := sheetOf(t, map[string]string{"A1": "=SEQUENCE(3)", "C1": "=SUM(A1:A3)", "A5": "end"})122+ if err := s.InsertRows(1, 1); err != nil {123+ t.Fatal(err)124+ }125+ wantShown(t, s, map[string]string{"A1": "1", "A2": "2", "A3": "3", "A4": "", "A6": "end", "C1": "6"})126+ s.DeleteRows(0, 1) // the anchor goes127+ wantShown(t, s, map[string]string{"A1": "", "A2": "", "A5": "end"})128+ if len(s.spills) != 0 {129+ t.Errorf("spills left: %v", s.spills)130+ }131+ s.Book().Undo()132+ s.Book().Undo()133+ wantShown(t, s, map[string]string{"A1": "1", "A3": "3", "A5": "end", "C1": "6"})134+}135+136+func TestSpillFormattingKept(t *testing.T) {137+ s := sheetOf(t, map[string]string{"A1": "=SEQUENCE(3)"})138+ s.SetStyle(rect("A2:A2"), func(st *Style) { st.Bold = true })139+ wantShown(t, s, map[string]string{"A2": "2"})140+ if c := s.Cell(at("A2")); !c.Spilled() || !c.Style.Bold {141+ t.Errorf("A2 = %+v, want spilled and bold", c)142+ }143+ s.Set(at("A1"), "=SEQUENCE(1)")144+ if c := s.Cell(at("A2")); c == nil || c.Spilled() || !c.Style.Bold || !c.Blank() {145+ t.Errorf("A2 after the spill shrank = %+v, want blank and bold", c)146+ }147+}148+149+func TestSpillCopiesValues(t *testing.T) {150+ s := sheetOf(t, map[string]string{"A1": "=SEQUENCE(3)"})151+ clip := s.Copy(rect("A1:A3"))152+ if _, err := s.Paste(clip, rect("C1:C1"), false); err != nil {153+ t.Fatal(err)154+ }155+ if got := s.Cell(at("C2")).Input; got != "2" {156+ t.Errorf("pasted spilled cell = %q, want its value", got)157+ }158+}@@ -65,6 +65,9 @@ type Workbook struct {
6565 // pivotDepth counts pivots refreshed because a pivot they read6666 // changed, to stop a loop; see pivotlayout.go.6767 pivotDepth int68+ // spillWork is the arrays an evaluation pass computed, to spill once69+ // it's done; see spill.go.70+ spillWork spillWork6871 }6972 7073 // loc is a cell on a particular sheet.@@ -22,6 +22,11 @@ const (
2222 Text2323 Bool2424 Error25+ // Array marks an array or LAMBDA while a formula is evaluated26+ // (internal/functions keeps them; Num says which). No cell ever holds27+ // one: a formula computing an array shows its first value and spills28+ // the rest.29+ Array2530 )2631 2732 // Value is the computed contents of a cell.