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 / value_test.go
value_test.go 6.5K
1package headless23import (4 "strings"5 "testing"67 "src.rest/cat/012/internal/sheet"8)910// Each type written as a Value is the cell typing it makes, shown as a11// person would type it, and reads back as the same Value.12func TestValuesBothWays(t *testing.T) {13 for _, c := range []struct {14 value, shown string15 format sheet.Format16 back string // what reads back, when not value17 }{18 {value: `3.5`, shown: "3.5"},19 {value: `"00123"`, shown: "00123"},20 {value: `"=1+1"`, shown: "=1+1"},21 {value: `true`, shown: "TRUE"},22 {value: `{"currency":3.5}`, shown: "$3.50", format: sheet.Format{Kind: sheet.FmtCurrency, Decimals: 2}},23 {value: `{"currency":-1200,"decimals":0}`, shown: "-$1,200", format: sheet.Format{Kind: sheet.FmtCurrency}},24 {value: `{"currency":3.5,"symbol":"€"}`, shown: "€3.50", format: sheet.Format{Kind: sheet.FmtCustom, Pattern: `"€"#,##0.00`}},25 {value: `{"percent":0.12}`, shown: "12%", format: sheet.Format{Kind: sheet.FmtPercent}},26 {value: `{"percent":0.125}`, shown: "12.50%", format: sheet.Format{Kind: sheet.FmtPercent, Decimals: 2}},27 {value: `{"date":"2026-09-29"}`, shown: "9/29/2026", format: sheet.Format{Kind: sheet.FmtDate}},28 {value: `{"date":"2026-09-29T14:30:00"}`, shown: "9/29/2026 14:30:00", format: sheet.Format{Kind: sheet.FmtDateTime}},29 {value: `{"date":"2026-09-29","format":"yyyy-mm-dd"}`, shown: "2026-09-29", format: sheet.Format{Kind: sheet.FmtDate, Pattern: "yyyy-mm-dd"}},30 {value: `{"time":"14:30"}`, shown: "2:30:00 PM", format: sheet.Format{Kind: sheet.FmtTime}, back: `{"time":"14:30:00"}`},31 {value: `{"duration":"90min"}`, shown: "1:30:00", format: sheet.Format{Kind: sheet.FmtDuration}},32 {value: `{"duration":5400}`, shown: "1:30:00", format: sheet.Format{Kind: sheet.FmtDuration}, back: `{"duration":"90min"}`},33 {value: `{"size":1500}`, shown: "1.5 kB", format: sheet.Format{Kind: sheet.FmtSize, Decimals: 1}},34 {value: `{"size":"1.5kb"}`, shown: "1.5 kB", format: sheet.Format{Kind: sheet.FmtSize, Decimals: 1}, back: `{"size":1500}`},35 {value: `{"number":1234.5,"decimals":2}`, shown: "1,234.50", format: sheet.Format{Kind: sheet.FmtNumber, Decimals: 2}},36 {value: `{"number":0.5,"format":"0.000"}`, shown: "0.500", format: sheet.Format{Kind: sheet.FmtCustom, Pattern: "0.000"}},37 {value: `{"text":"12%"}`, shown: "12%", back: `"12%"`},38 } {39 w := sheet.NewBook()40 s := w.Sheet(0)41 if _, err := Set(w, []Entry{{Ref: "B2", Value: Value(c.value)}}, SetOptions{}); err != nil {42 t.Errorf("%s: %v", c.value, err)43 continue44 }45 a := addr("B2")46 if got := s.LocalText(a); got != c.shown {47 t.Errorf("%s shows %q, want %q", c.value, got, c.shown)48 }49 if got := s.DisplayFormat(a); got != c.format {50 t.Errorf("%s is formatted %+v, want %+v", c.value, got, c.format)51 }52 back := c.back53 if back == "" {54 back = c.value55 }56 if got := string(CellValue(s, a)); got != back {57 t.Errorf("%s reads back as %s, want %s", c.value, got, back)58 }59 }60}6162// Cells typed in the grid read as Values of their types.63func TestTypedEntriesRead(t *testing.T) {64 w := sheet.NewBook()65 s := w.Sheet(0)66 for in, want := range map[string]string{67 "$3.50": `{"currency":3.5}`,68 "12%": `{"percent":0.12}`,69 "2026-09-29": `{"date":"2026-09-29","format":"yyyy-mm-dd"}`,70 "1,234": `{"number":1234,"decimals":0}`,71 "'00123": `"00123"`,72 "25:30:00": `{"duration":"1530min"}`,73 } {74 if _, err := Set(w, []Entry{{Ref: "A1", Input: in}}, SetOptions{}); err != nil {75 t.Fatal(err)76 }77 if got := string(CellValue(s, addr("A1"))); got != want {78 t.Errorf("%s reads as %s, want %s", in, got, want)79 }80 }81}8283func TestValueErrors(t *testing.T) {84 for value, want := range map[string]string{85 `{"price":3}`: `"price" isn't a field of a value`,86 `{"currency":"3"}`: `currency: "3" is a number`,87 `{"date":"29/9/2026"}`: `date: "29/9/2026" isn't a date`,88 `{"size":"3 apples"}`: `size: "3 apples" isn't nushell's size`,89 `{"duration":"1kb"}`: `duration: 1000b isn't a duration`,90 `[1, 2]`: `isn't a cell's value`,91 `{"decimals":2}`: `names its type`,92 `{"currency":1,"percent":0.5}`: `"percent" isn't a field`,93 } {94 _, err := Set(sheet.NewBook(), []Entry{{Ref: "A1", Value: Value(value)}}, SetOptions{})95 if err == nil || !strings.Contains(err.Error(), want) {96 t.Errorf("%s: %v, want %q", value, err, want)97 }98 }99 if _, err := Set(sheet.NewBook(), []Entry{{Ref: "A1", Input: "1", Value: Value("1")}}, SetOptions{}); err == nil {100 t.Error("input and value both taken")101 }102}103104// A format alone formats a range; with an input it formats the cell.105func TestEntryFormats(t *testing.T) {106 w := sheet.NewBook()107 s := w.Sheet(0)108 _, err := Set(w, []Entry{{Ref: "A1", Input: "3.5"}, {Ref: "A2", Input: "4"}, {Ref: "A1:A2", Format: `"$"#,##0.00`}, {Ref: "B1", Input: "0.25", Format: "0.0%"}}, SetOptions{})109 if err != nil {110 t.Fatal(err)111 }112 for a, want := range map[string]string{"A1": "$3.50", "A2": "$4.00", "B1": "25.0%"} {113 if got := s.LocalText(addr(a)); got != want {114 t.Errorf("%s shows %q, want %q", a, got, want)115 }116 }117}118119// A table written as records gets a header, typed cells and column120// formats, blanks and plain numbers taking their column's.121func TestWriteTable(t *testing.T) {122 w := sheet.NewBook()123 s := w.Sheet(0)124 rows := []Row{125 Row(`{"Item": "Tea", "Price": {"currency": 3.5}, "Bought": {"date": "2026-09-29"}, "Code": "00123"}`),126 Row(`{"Item": "Cake", "Price": 3, "Code": "7", "Share": 0.5}`),127 Row(`{Item: Jam, Price: null, Bought: 2026-09-28T10:00:00Z, Size: 1.5kb}`),128 }129 warn, err := WriteTable(w, TableSpec{At: "B2", Rows: rows, Formats: map[string]string{"Share": "0%"}}, SetOptions{})130 if err != nil || len(warn) != 0 {131 t.Fatal(err, warn)132 }133 for a, want := range map[string]string{"B2": "Item", "C2": "Price", "G2": "Size", "C3": "$3.50", "C4": "$3.00", "D3": "9/29/2026",134 "E3": "00123", "E4": "7", "F4": "50%", "G5": "1.5 kB"} {135 if got := s.LocalText(addr(a)); got != want {136 t.Errorf("%s shows %q, want %q", a, got, want)137 }138 }139 if f := s.DisplayFormat(addr("C5")); f.Kind != sheet.FmtCurrency {140 t.Errorf("the blank price is formatted %+v", f)141 }142 if f := s.DisplayFormat(addr("D5")); f.Kind != sheet.FmtDateTime {143 t.Errorf("a date and time in a column of dates is formatted %+v", f)144 }145 if v := s.Value(addr("E4")); v.Kind != sheet.Text {146 t.Errorf("text that looks like a number is %+v", v)147 }148 if _, err := WriteTable(w, TableSpec{At: "A1", Rows: []Row{Row(`[1]`)}}, SetOptions{}); err == nil || !strings.Contains(err.Error(), "row 1 isn't a record") {149 t.Errorf("a row that isn't a record: %v", err)150 }151}