cat / 012
clone

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

cat/module-path
012 / internal / headless / table.go
table.go 5.7K
1package headless23import (4 "encoding/json"5 "fmt"67 "src.rest/cat/012/internal/nuon"8 "src.rest/cat/012/internal/sheet"9)1011// TableSpec is a table to write: rows of records, each field a Value,12// under a header row naming their fields, as importing NUON stores a13// table. Columns orders them: a JSON object's keys may arrive in14// another order than written.15type TableSpec struct {16 At string `json:"at" jsonschema:"the table's top-left cell, where its header goes: A1, Q3!B2"`17 Columns []string `json:"columns,omitempty" jsonschema:"the columns' names in order, left to right; rows' other fields follow them. Give it: a record's fields may arrive in any order"`18 Rows []Row `json:"rows" jsonschema:"the rows, each a record of the columns' values or a list of them in columns' order, typed as a cell's value is: {\"Item\": \"Tea\", \"Price\": {\"currency\": 3.5}, \"Bought\": {\"date\": \"2026-09-29\"}} or [\"Tea\", {\"currency\": 3.5}, {\"date\": \"2026-09-29\"}]"`19 Formats map[string]string `json:"formats,omitempty" jsonschema:"number format codes by column name, for every cell of the column under the header: {\"Price\": \"$#,##0.00\"}"`20}2122// Row is a table's row, in JSON or NUON: a record of its columns'23// Values by name, or a list of them in the columns' order.24type Row json.RawMessage2526// MarshalJSON writes r as it is.27func (r Row) MarshalJSON() ([]byte, error) { return Value(r).MarshalJSON() }2829// UnmarshalJSON keeps the JSON as it is.30func (r *Row) UnmarshalJSON(b []byte) error { return (*Value)(r).UnmarshalJSON(b) }3132// WriteTable writes spec's rows under a header naming their columns,33// in the order they're first seen. Each column takes the format of its34// first value that has one (currency, a date, a size), or the one35// Formats gives it, so its plain numbers and blanks show as the rest36// do; a cell whose own value has another type keeps its own. Each cell37// is checked as Set checks it.38func WriteTable(w *sheet.Workbook, spec TableSpec, o SetOptions) (warnings []string, err error) {39 err = w.Batch(sheet.Change{Label: "write table"}, func() (err error) {40 warnings, err = writeTable(w, spec, o)41 return err42 })43 return warnings, err44}4546func writeTable(w *sheet.Workbook, spec TableSpec, o SetOptions) (warnings []string, err error) {47 s, at, err := ResolveCell(w, spec.At)48 if err != nil {49 return nil, err50 }51 cols, rows, err := tableRows(spec.Rows, spec.Columns)52 if err != nil {53 return nil, err54 }55 if at.Col+len(cols) > sheet.MaxCols || at.Row+len(rows) >= sheet.MaxRows {56 return nil, fmt.Errorf("a table of %d rows and %d columns doesn't fit at %s", len(rows), len(cols), spec.At)57 }58 formats := columnFormats(cols, rows, spec.Formats)59 for i, name := range cols {60 t := typed{input: textInputOf(name)}61 if warn, err := putTyped(s, sheet.Addr{Col: at.Col + i, Row: at.Row}, t, o); err != nil {62 return warnings, err63 } else if warn != "" {64 warnings = append(warnings, warn)65 }66 }67 for r, row := range rows {68 for i, t := range row {69 if f, ok := formats[i]; ok && (!t.formatted || spec.Formats[cols[i]] != "") {70 t.format, t.formatted = f, true71 }72 warn, err := putTyped(s, sheet.Addr{Col: at.Col + i, Row: at.Row + 1 + r}, t, o)73 if err != nil {74 return warnings, err75 }76 if warn != "" {77 warnings = append(warnings, warn)78 }79 }80 }81 return warnings, nil82}8384// tableRows reads rows as the columns they name, columns first and the85// others in the order first seen, and each row's cells in those86// columns. A row is a record, or a list of values in columns' order.87func tableRows(values []Row, columns []string) ([]string, [][]typed, error) {88 t := tableReader{index: map[string]int{}}89 for _, c := range columns {90 t.col(c)91 }92 for n, v := range values {93 val, err := nuon.Parse(v)94 if err == nil && val.Kind == nuon.List {95 val, err = t.record(val.List, len(columns))96 }97 if err != nil || val.Kind != nuon.Record {98 return nil, nil, fmt.Errorf("row %d isn't a record of the columns' values, or a list of them in columns' order: %s", n+1, v)99 }100 row := make([]typed, len(t.cols))101 for _, f := range val.Fields {102 i := t.col(f.Key)103 c, err := typedOf(f.Value)104 if err != nil {105 return nil, nil, fmt.Errorf("row %d, %s: %w", n+1, f.Key, err)106 }107 for len(row) <= i {108 row = append(row, typed{})109 }110 row[i] = c111 }112 t.rows = append(t.rows, row)113 }114 return t.cols, t.rows, nil115}116117// tableReader collects a table's columns and rows.118type tableReader struct {119 cols []string120 index map[string]int121 rows [][]typed122}123124// col is the column named name, added after the others when new.125func (t *tableReader) col(name string) int {126 i, ok := t.index[name]127 if !ok {128 i = len(t.cols)129 t.index[name] = i130 t.cols = append(t.cols, name)131 }132 return i133}134135// record is a list of values as a record of the first n columns.136func (t *tableReader) record(vs []nuon.Value, n int) (nuon.Value, error) {137 if len(vs) > n {138 return nuon.Value{}, fmt.Errorf("%d values for %d columns", len(vs), n)139 }140 fs := make([]nuon.Field, len(vs))141 for i, v := range vs {142 fs[i] = nuon.Field{Key: t.cols[i], Value: v}143 }144 return nuon.RecordValue(fs...), nil145}146147// columnFormats are the formats of the table's columns, by index: the148// one formats names, or the first a value sets.149func columnFormats(cols []string, rows [][]typed, formats map[string]string) map[int]sheet.Format {150 out := map[int]sheet.Format{}151 for i, name := range cols {152 if code := formats[name]; code != "" {153 out[i] = FormatOfCode(code)154 continue155 }156 for _, row := range rows {157 if i < len(row) && row[i].formatted {158 out[i] = row[i].format159 break160 }161 }162 }163 return out164}165166// textInputOf is a column's name as an entry: text, whatever it looks167// like.168func textInputOf(name string) string {169 t, _ := typedOf(nuon.StringValue(name))170 return t.input171}