cat / 012
clone

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

main
012 / internal / sheet / slot_test.go
slot_test.go 4.7K
1package sheet23import (4 "reflect"5 "strings"6 "testing"78 "src.rest/cat/012/internal/formula"9)1011// literalValue is what recalculation computes for an entry that isn't a12// formula (evaluate.go).13func literalValue(c *Cell) Value {14 switch e := c.expr.(type) {15 case formula.Num:16 return num(e.V)17 case formula.Bool:18 return boolean(e.V)19 }20 switch {21 case c.Input == "":22 return Value{}23 case c.Format.Kind == FmtText:24 return Value{Kind: Text, Str: c.Input}25 }26 return Value{Kind: Text, Str: strings.TrimPrefix(c.Input, "'")}27}2829// roundTripCells are cells of every kind: entries of each sort under30// formats, styles and notes, with the values recalculation gives them.31func roundTripCells() []*Cell {32 inputs := []string{33 "1", "1.50", "-0", "0.1", "1e5", "007", "1.0000000000000000001", "123456789012345678901234567890",34 "$1,200", "12%", "9/26/2026", "2026-09-26", "13:45", "TRUE", "true", "FALSE", "hello", "'123", "'",35 "=A1", "=5", "=TRUE", "+5", "-5", "+A1", " 7 ", "ünïcode", "#N/A", "1e400", "",36 }37 formats := []Format{{}, Preset(FmtCurrency), {Kind: FmtText}, {Kind: FmtDate, Pattern: "yyyy-mm-dd"}}38 styles := []Style{{}, {Bold: true}, {Align: AlignRight, own: true}}39 var out []*Cell40 for i := range len(inputs) * len(formats) * len(styles) * 4 {41 input, f := inputs[i%len(inputs)], formats[i/len(inputs)%len(formats)]42 sty, rest := styles[i/len(inputs)/len(formats)%len(styles)], i/len(inputs)/len(formats)/len(styles)43 c, err := newCell(input, f, sty, rest&1 == 1)44 if rest&2 == 2 {45 c = c.withNote("a note")46 }47 if err != nil || c == nil {48 continue49 }50 c.Value = literalValue(c)51 if c.expr != nil && !c.literal() {52 c.Value = Value{} // a formula: only recalculation sets it53 }54 out = append(out, c)55 }56 return out57}5859// Every cell comes back from the store as it went in, whether it is kept60// as a plain slot or whole: its entry exactly as typed, its value, format,61// style, note and expression.62func TestSlotRoundTrip(t *testing.T) {63 st := newCellStore()64 a := Addr{Col: 3, Row: 1500}65 cells := roundTripCells()66 for _, c := range cells {67 want := c.clone()68 st.set(a, c)69 checkSame(t, st.get(a), want)70 checkSame(t, st.copyOf(a), want)71 if v := st.value(a); v != want.Value {72 t.Errorf("%q: value %v, want %v", want.Input, v, want.Value)73 }74 if got := st.filledAt(a); got != !want.Blank() {75 t.Errorf("%q: filled %v", want.Input, got)76 }77 if !st.holds(a, st.get(a)) {78 t.Errorf("%q: doesn't hold its own view", want.Input)79 }80 }81 st.delete(a)82 if st.len() != 0 || len(st.strs.index) != 0 || len(st.rich) != len(st.richFree) {83 t.Errorf("after %d cells: %d stored, %d strings, %d rich", len(cells), st.len(), len(st.strs.index), len(st.rich)-len(st.richFree))84 }85}8687// literal reports whether c's expression is a typed number or boolean,88// which recalculation doesn't evaluate as a formula.89func (c *Cell) literal() bool {90 switch c.expr.(type) {91 case formula.Num, formula.Bool:92 return !strings.HasPrefix(c.Input, "=")93 }94 return false95}9697func checkSame(t *testing.T, got, want *Cell) {98 t.Helper()99 if got.Input != want.Input || got.Value != want.Value || got.Format != want.Format || got.Style != want.Style ||100 got.Note != want.Note || got.auto != want.auto || got.derived != want.derived || got.spilled != want.spilled ||101 !reflect.DeepEqual(got.expr, want.expr) || got.IsFormula() != want.IsFormula() {102 t.Errorf("stored %+v\ncame back %+v", want, got)103 }104}105106// Cells sharing a string keep it while any of them uses it, and a cell107// turning from plain to rich and back keeps its place.108func TestSlotStrings(t *testing.T) {109 st := newCellStore()110 a1, a2 := Addr{}, Addr{Row: 1}111 st.set(a1, &Cell{Input: "same"})112 st.set(a2, &Cell{Input: "same"})113 if len(st.strs.index) != 1 {114 t.Fatalf("%d strings for one text", len(st.strs.index))115 }116 st.delete(a1)117 if c := st.get(a2); c.Input != "same" || c.Value.Str != "same" {118 t.Errorf("the other cell is %+v", c)119 }120 rich := &Cell{Input: "same", Note: "n"}121 st.set(a2, rich)122 if st.get(a2) != rich || st.richAt(a2) != rich || len(st.strs.index) != 0 {123 t.Errorf("rich cell not kept whole, %d strings", len(st.strs.index))124 }125 num, _ := newCell("12.50", Format{}, Style{}, true)126 st.set(a2, num)127 if c := st.get(a2); c.Input != "12.50" || c.Value.Num != 12.5 || st.richAt(a2) != nil {128 t.Errorf("back to plain: %+v", c)129 }130 if len(st.rich) != len(st.richFree) {131 t.Errorf("%d rich cells left", len(st.rich)-len(st.richFree))132 }133}134135// A plain number costs its slot: no string, no Cell.136func TestSlotCompact(t *testing.T) {137 s := New()138 for row := range 2048 {139 s.Load(Addr{Row: row}, []string{"1", "2.5", "3.10", "-4"}[row%4], Format{}, Style{})140 }141 if n := len(s.cells.rich) + len(s.cells.strs.strs); n != 0 {142 t.Errorf("%d rich cells and strings for plain numbers", n)143 }144}