- commit
- 3be58e82b6b0afd86ca696ab42f9f85c1a4ffb1e
- parent
- c984e368941272401bafa315a49fd931607cbf1c
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 1.6 of 3, Jev's estimate
| M | docs/ | 3 | +- |
| M | internal/ | 39 | ++++++++++++++++++++++++++++- |
| M | internal/ | 11 | ++++++-- |
| M | internal/ | 41 | ++++++++++++++++++++++++++++++ |
@@ -30,7 +30,8 @@ it isn't the one shown when the file was saved:
3030 |---|---|3131 | `B7`, `A1:C9`, `A:A`, `$B$7` | Cells of the sheet shown when the file was saved |3232 | `Q3!B7`, `'Q3 plan'!A1:C9` | Cells of the sheet named, quoted as in formulas when the name has spaces (the shell needs quotes around it too) |33-| `Sales` | A [named range](../formulas/references.md) |33+| `Sales` | A [named range](../formulas/references.md), or a [table](../sheets/tables.md) or notebook output, header row included, so JSON and NUON name its columns |34+| `Sales[Amount]`, `Sales[[#Headers],[Amount]]` | A table's cells by a [structured reference](../formulas/references.md#tables-by-column-name), as a formula outside the table reads them: `Sales[Amount]` is the column's data, without its header |3435 | `Q3`, `Q3!` | The whole sheet, from A1 to its last cell with contents |3536 3637 A reference that names nothing says what the workbook has: `no sheet@@ -63,7 +63,7 @@ func TestResolve(t *testing.T) {
6363 }6464 for ref, want := range map[string]string{6565 "Q4!A1": `no sheet named "Q4" (sheets: Sheet1, Q3 plan)`,66- "what": `"what" isn't a cell, a range, a named range or a sheet`,66+ "what": `"what" isn't a cell, a range, a named range, a table or a sheet`,6767 } {6868 if _, err := Resolve(w, ref); err == nil || !strings.Contains(err.Error(), want) {6969 t.Errorf("Resolve(%q): %v, want %q", ref, err, want)@@ -74,6 +74,43 @@ func TestResolve(t *testing.T) {
7474 }7575 }7676 77+// Tables are found by name, whole, or by structured references as78+// formulas read them; set types into a cell one names.79+func TestTables(t *testing.T) {80+ w := book(t)81+ if err := w.Sheet(0).CreateTable("Fruit", sheet.NewRect(addr("A1"), addr("B3"))); err != nil {82+ t.Fatal(err)83+ }84+ for ref, want := range map[string]string{85+ "Fruit": "Sheet1!A1:B3",86+ "fruit[Price]": "Sheet1!B2:B3",87+ "Fruit[#All]": "Sheet1!A1:B3",88+ "Fruit[[#Headers],[Price]]": "Sheet1!B1",89+ } {90+ if got, err := Resolve(w, ref); err != nil || got.String() != want {91+ t.Errorf("Resolve(%q) = %s, %v; want %s", ref, got, err, want)92+ }93+ }94+ for ref, want := range map[string]string{95+ "Fruit[Cost]": "Fruit has no such column (columns: Item, Price)",96+ "Fruit[@Item]": "names no rows of Fruit here",97+ "Veg[Item]": "there's no table named Veg",98+ } {99+ if _, err := Resolve(w, ref); err == nil || !strings.Contains(err.Error(), want) {100+ t.Errorf("Resolve(%q): %v, want %q", ref, err, want)101+ }102+ }103+ if got := get(t, w, "Fruit", GetOptions{Format: "nuon"}); got != "[[Item, Price]; [Apple, 1.5],\n[Pear, 2]]\n" {104+ t.Errorf("get Fruit %q", got)105+ }106+ if _, err := Set(w, []Entry{{"Fruit[[#Headers],[Price]]", "Cost"}}, SetOptions{}); err != nil {107+ t.Fatal(err)108+ }109+ if got := get(t, w, "Fruit[[#Headers],[Cost]]", GetOptions{}); got != "Cost\n" {110+ t.Errorf("the header set %q", got)111+ }112+}113+77114 func get(t *testing.T, w *sheet.Workbook, ref string, o GetOptions) string {78115 t.Helper()79116 target, err := Resolve(w, ref)@@ -28,8 +28,10 @@ func (t Target) String() string {
2828 2929 // Resolve finds what ref names in w: a cell or range (B7, A1:C9, A:A),3030 // on a sheet (Q3!B7, 'Q3 plan'!A1:C9) or else the sheet shown when the31-// file was saved; a named range (Sales); or a sheet by name (Q3, or32-// Q3!), meaning all of it. "" is the whole sheet shown.31+// file was saved; a named range (Sales); a table by name, all of it32+// with its header row, or by a structured reference as formulas read it33+// (Sales[Amount]); or a sheet by name (Q3, or Q3!), meaning all of it.34+// "" is the whole sheet shown.3335 func Resolve(w *sheet.Workbook, ref string) (Target, error) {3436 ref = strings.TrimSpace(ref)3537 shown := w.Sheet(w.Active())@@ -56,11 +58,14 @@ func Resolve(w *sheet.Workbook, ref string) (Target, error) {
5658 }5759 return Target{Sheet: n.Sheet, Range: n.Range}, nil5860 }61+ if s, r, ok, err := w.TableRange(rest); ok {62+ return Target{Sheet: s, Range: r}, err63+ }5964 if s := w.Lookup(rest); s != nil {6065 return Target{Sheet: s, Whole: true}, nil6166 }6267 }63- return Target{}, fmt.Errorf("%q isn't a cell, a range, a named range or a sheet (sheets: %s)", ref, sheetList(w))68+ return Target{}, fmt.Errorf("%q isn't a cell, a range, a named range, a table or a sheet (sheets: %s)", ref, sheetList(w))6469 }6570 6671 // ResolveCell is Resolve for a reference that must be one cell.@@ -1,6 +1,7 @@
11 package sheet22 33 import (4+ "fmt"45 "slices"56 "strings"67 @@ -334,3 +335,43 @@ func (s *Sheet) TableLook(a Addr) (header, band bool) {
334335 }335336 return false, false336337 }338+339+// TableRange finds the cells a structured reference written alone340+// stands for (Sales[Amount], Sales[#All]), as a formula outside the341+// table reads them, or with a table's name alone its whole table,342+// header row included. ok is false when ref is neither; err says why343+// one names no cells.344+func (w *Workbook) TableRange(ref string) (s *Sheet, r Rect, ok bool, err error) {345+ n, perr := formula.Parse(ref, parserFuncs)346+ if perr != nil {347+ return nil, Rect{}, false, nil348+ }349+ var t formula.TableRef350+ switch n := n.(type) {351+ case formula.TableRef:352+ t = n353+ case formula.Name:354+ t = formula.TableRef{Table: n.Name, Items: formula.ItemAll}355+ default:356+ return nil, Rect{}, false, nil357+ }358+ v, found := w.findTable(nameKey(t.Table))359+ switch {360+ case !found:361+ if _, isName := n.(formula.Name); isName {362+ return nil, Rect{}, false, nil363+ }364+ return nil, Rect{}, true, fmt.Errorf("there's no table named %s", t.Table)365+ case !v.ok:366+ return nil, Rect{}, true, fmt.Errorf("%s has no rows yet", t.Table)367+ }368+ lo, hi, okRows := v.rows(t.Rows(), Addr{}, false)369+ c0, c1, okCols := v.colSpan(t)370+ switch {371+ case !okCols:372+ return nil, Rect{}, true, fmt.Errorf("%s has no such column (columns: %s)", t.Table, strings.Join(v.cols, ", "))373+ case !okRows:374+ return nil, Rect{}, true, fmt.Errorf("%s names no rows of %s here: a formula's own row, or rows it hasn't", ref, t.Table)375+ }376+ return v.s, Rect{From: Addr{Col: c0, Row: lo}, To: Addr{Col: c1, Row: hi}}, true, nil377+}