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 / sheet / functions_jev_test.go
functions_jev_test.go 5.0K
1package sheet23import (4 "reflect"5 "testing"6)78// fakeRemote answers from a table and records every call it's asked.9type fakeRemote struct {10 answers map[string]RemoteAnswer // by Instructions11 asked []RemoteCall12}1314func (f *fakeRemote) Lookup(c RemoteCall) (RemoteAnswer, bool) {15 f.asked = append(f.asked, c)16 a, ok := f.answers[c.Instructions]17 return a, ok18}1920func TestJEVFunctions(t *testing.T) {21 f := &fakeRemote{answers: map[string]RemoteAnswer{22 "Is this a complaint?": {Noul: 0.83},23 "Sentiment": {Choice: "negative", Confidence: 0.9},24 "Urgency": {Score: 1.6, Confidence: 0.7},25 "Broken": {Failed: "HTTP 500"},26 }}27 s := New()28 s.SetRemote(f)29 s.Set(at("A1"), "The package arrived crushed and support never replied.")30 s.Set(at("B1"), "positive")31 s.Set(at("B2"), "negative")32 s.Set(at("B3"), "neutral")3334 tests := []struct {35 formula string36 want Value37 }{38 {`=JEV.TEST(A1, "Is this a complaint?")`, boolean(true)},39 {`=JEV.PROB(A1, "Is this a complaint?")`, num(0.83)},40 {`=JEV.CLASSIFY(A1, "Sentiment", B1:B3)`, Value{Kind: Text, Str: "negative"}},41 {`=JEV.CLASSIFY(A1, "Sentiment", "positive, negative, neutral")`, Value{Kind: Text, Str: "negative"}},42 {`=JEV.SCORE(A1, "Urgency", "low, medium, high")`, num(1.6)},43 {`=JEV.TEST(A1, "Not answered yet")`, Pending},44 {`=JEV.TEST(A1, "Broken")`, ErrRemote},45 {`=JEV.TEST(1/0, "Is this a complaint?")`, ErrDiv0}, // bad input isn't sent46 {`=JEV.CLASSIFY(A1, "Sentiment", "only one")`, ErrValue}, // nothing to choose47 {`=IF(JEV.TEST(A1, "Is this a complaint?"), "escalate", "ok")`, Value{Kind: Text, Str: "escalate"}},48 }49 for _, tt := range tests {50 if err := s.Set(at("C1"), tt.formula); err != nil {51 t.Fatalf("Set(%q): %v", tt.formula, err)52 }53 if got := s.Value(at("C1")); got != tt.want {54 t.Errorf("%s = %+v, want %+v", tt.formula, got, tt.want)55 }56 }57 // JEV.PROB shows as a percent.58 s.Set(at("C1"), `=JEV.PROB(A1, "Is this a complaint?")`)59 if got := s.DisplayFormat(at("C1")).Kind; got != FmtPercent {60 t.Errorf("JEV.PROB format %v", got)61 }62}6364func TestJEVCalls(t *testing.T) {65 f := &fakeRemote{}66 s := New()67 s.SetRemote(f)68 s.Set(at("A1"), "text")69 s.Set(at("A2"), "42")70 s.Set(at("B1"), "good")71 s.Set(at("B2"), "bad")72 s.Set(at("C1"), "It works")73 s.Set(at("C2"), "It doesn't")74 s.Set(at("D1"), `=JEV.CLASSIFY(A1:A2, "Quality", B1:B2, C1:C2)`)75 calls := s.RemoteCalls(at("D1"))76 want := RemoteCall{77 Kind: "choice",78 State: [][]any{{"text"}, {42.0}},79 Instructions: "Quality",80 Criteria: map[string]string{"good": "It works", "bad": "It doesn't"},81 }82 if len(calls) != 1 || !reflect.DeepEqual(calls[0], want) {83 t.Errorf("calls %#v", calls)84 }85 s.Set(at("D2"), `=JEV.TEST(A1, "Q", "yes means this")`)86 if c := s.RemoteCalls(at("D2")); len(c) != 1 || c[0].Criteria != [2]string{"yes means this", ""} {87 t.Errorf("noul criteria %#v", c)88 }89 // Answers arriving later show up after RecalcVolatile.90 f.answers = map[string]RemoteAnswer{"Q": {Noul: 0.1}}91 if s.Value(at("D2")) != Pending {92 t.Fatalf("D2 = %+v before answers", s.Value(at("D2")))93 }94 s.Set(at("E1"), "=IF(D2, 1, 2)")95 state := s.StateID()96 s.RecalcVolatile()97 if s.Value(at("D2")) != boolean(false) || s.Value(at("E1")).Num != 2 {98 t.Errorf("after answers: D2 %+v E1 %+v", s.Value(at("D2")), s.Value(at("E1")))99 }100 if s.StateID() != state {101 t.Error("answers arriving counted as an edit")102 }103}104105func TestJEVWithoutKey(t *testing.T) {106 s := New()107 s.Set(at("A1"), `=JEV.TEST("x", "Q")`)108 if s.Value(at("A1")) != ErrNoRemote {109 t.Errorf("no key: %+v", s.Value(at("A1")))110 }111}112113// An answer recalculates the formulas that asked it and what reads them,114// not every JEV formula.115func TestRecalcAnswered(t *testing.T) {116 f := &fakeRemote{answers: map[string]RemoteAnswer{}}117 s := New()118 s.SetRemote(f)119 s.Set(at("A1"), `=JEV.TEST("x", "Q1")`)120 s.Set(at("A2"), `=JEV.TEST("x", "Q2")`)121 s.Set(at("A3"), `=IF(JEV.TEST("x", "Q1"), "yes", "no")`)122 s.Set(at("B1"), `=A1`)123 if !IsPending(s.Value(at("A1"))) || !IsPending(s.Value(at("A3"))) {124 t.Fatalf("A1 %+v, A3 %+v", s.Value(at("A1")), s.Value(at("A3")))125 }126 f.answers["Q1"] = RemoteAnswer{Noul: 0.9}127 f.asked = nil128 q1 := RemoteCall{Kind: "noul", State: "x", Instructions: "Q1", Criteria: [2]string{}}129 s.Book().RecalcAnswered([]RemoteCall{q1})130 for _, c := range f.asked {131 if c.Instructions != "Q1" {132 t.Errorf("an answer to Q1 asked %q again", c.Instructions)133 }134 }135 if len(f.asked) != 2 {136 t.Errorf("%d questions looked up, want the 2 cells asking Q1", len(f.asked))137 }138 if got := s.Value(at("B1")); got != boolean(true) {139 t.Errorf("B1, reading A1, = %+v", got)140 }141 if got := s.Value(at("A3")); got.Str != "yes" {142 t.Errorf("A3 = %+v", got)143 }144 if !IsPending(s.Value(at("A2"))) {145 t.Errorf("A2 = %+v", s.Value(at("A2")))146 }147 // Answered questions are forgotten; an unknown one does nothing.148 f.asked = nil149 s.Book().RecalcAnswered([]RemoteCall{q1, {Kind: "noul", Instructions: "Q9"}})150 if len(f.asked) != 0 {151 t.Errorf("answering again looked up %d questions", len(f.asked))152 }153}