cat / 012
clone

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

Spill a range that is a cell's whole formula, as Sheets does

Dami committed 68b9324
=B2:B9 and =Rent now spill rather than read the cell in the formula's
row: Sheets and Excel 365 both spill them. A range with an operator,
or as an argument taking one value, still intersects.

The oracle checks the array functions excelize computes (UNIQUE,
TRANSPOSE, TEXTJOIN over arrays, array literals) and, for the rest, the
value Sheets gives, with the reason excelize can't; its intersection
cases keep testing intersection with a unary plus.

Co-Authored-By: Claude <noreply@anthropic.com>
commit
68b9324734e99ef024ba85f906b516457aa22a29
parent
7497bb78c112b4528295fa7bb254e312f8ee4d70
author
Dami <dami.etoile@gmail.com>
6 files changed, +96 -13
Minternal/functions/dynamic.go8+++-
Minternal/functions/eval.go6++-
Minternal/sheet/explain_test.go2+-
Minternal/sheet/intersect_test.go11+++--
Aoracle/arrays_test.go72++++++++++++++++++++++++++++++
Moracle/intersect_test.go10+++--
M internal/functions/dynamic.go +6 -2
M internal/functions/eval.go +5 -1
M internal/sheet/explain_test.go +1 -1
M internal/sheet/intersect_test.go +6 -5
@@ -7,7 +7,8 @@ import (
77 88 // A range where one value is wanted reads as the cell in the formula's99 // row (a column) or column (a row), as in Sheets and Excel; functions10-// that take ranges still read all of them.10+// that take ranges still read all of them, and a range that is a cell's11+// whole formula spills, as in Sheets.1112 func TestImplicitIntersection(t *testing.T) {1213 for _, decimal := range []bool{false, true} {1314 s := sheetOf(t, map[string]string{
@@ -15,7 +16,7 @@ func TestImplicitIntersection(t *testing.T) {
1516 "B2": "1400", "B3": "1450", "B4": "1500",1617 "B6": "10", "C6": "20", "D6": "30",1718 "F2": "=B2:B4*2", "F3": "=Rent*2", "F4": "=Rent*2", "F5": "=Rent*2",18- "G3": "=B2:B4", "G4": "=ABS(-Rent)", "G5": "=B:B+1",19+ "G3": "=+B2:B4", "G4": "=ABS(-Rent)", "G5": "=B:B+1",1920 "H3": "=SUM(Rent)", "H4": "=SUMPRODUCT(Rent)", "H2": "=MATCH(1450,Rent,0)",2021 "H5": "=SUM(Rent*2)", "H6": "=SUMPRODUCT(Rent*2)",2122 "C7": "=B6:D6+1", "E7": "=B6:D6", "A8": "=B2:C4",
@@ -30,7 +31,7 @@ func TestImplicitIntersection(t *testing.T) {
3031 "G3": "1450", "G4": "1500", "G5": "1",3132 "H3": "4350", "H4": "4350", "H2": "2",3233 "H5": "8700", "H6": "8700", // array arguments: computed over the whole range33- "C7": "21", "E7": "#VALUE!", "A8": "#VALUE!",34+ "C7": "21", "E7": "10", "G7": "30", "A8": "1400", "A10": "1500", // E7 and A8 spill3435 "J3": "1450", "K3": "1450!", "L3": "2",3536 } {3637 if got := s.Value(at(a)).String(); got != want {
@@ -62,13 +63,13 @@ func TestImplicitIntersectionOtherSheet(t *testing.T) {
6263 }6364 for a, in := range map[string]string{6465 "A3": "='Q3 plan'!C2:C4*10", "A4": "=Plan+1", "A5": "=Plan",65- "C1": "='Q3 plan'!B9:D9", "E1": "='Q3 plan'!B9:D9",66+ "C1": "=+'Q3 plan'!B9:D9", "E1": "=+'Q3 plan'!B9:D9",6667 } {6768 if err := s.Set(at(a), in); err != nil {6869 t.Fatal(err)6970 }7071 }71- for a, want := range map[string]string{"A3": "60", "A4": "8", "A5": "#VALUE!", "C1": "2", "E1": "#VALUE!"} {72+ for a, want := range map[string]string{"A3": "60", "A4": "8", "A5": "5", "A7": "7", "C1": "2", "E1": "#VALUE!"} { // A5 spills7273 if got := s.Value(at(a)).String(); got != want {7374 t.Errorf("%s = %s, want %s", a, got, want)7475 }
A oracle/arrays_test.go +72 -0
@@ -0,0 +1,72 @@
1+package oracle2+3+import (4+ "strings"5+ "testing"6+7+ "github.com/xuri/excelize/v2"8+)9+10+// arrayFormulas compute over arrays; each is compared by the value its11+// cell shows (an array's first). Those excelize can't compute have the12+// value Sheets gives and the reason, and 012 is checked against that.13+var arrayFormulas = []struct{ formula, sheets, why string }{14+ {`=UNIQUE(G1:G4)`, "", ""},15+ {`=TRANSPOSE(C1:D2)`, "", ""},16+ {`=INDEX(TRANSPOSE(C1:D2), 2, 1)`, "", ""},17+ {`=TEXTJOIN(",", TRUE, UNIQUE(D1:D5))`, "", ""},18+ {`=SUM({1,2,3})`, "", ""},19+ {`=SUMPRODUCT((B1:B5="north")*C1:C5)`, "9", "excelize reads a range in an expression as its first cell"},20+ {`=SUMPRODUCT(C1:C3*G1:G3)`, "11", "excelize reads a range in an expression as its first cell"},21+ {`=SUMPRODUCT(--(C1:C7>3))`, "4", "excelize doesn't negate a comparison of ranges"},22+ {`=ROWS(UNIQUE(D1:D5))`, "4", "excelize's ROWS takes only a reference"},23+ {`=INDEX({10,20,30}, 2)`, "20", "excelize's INDEX takes only a reference"},24+ {`=SUM(C1:C3*2)`, "12", "excelize doesn't compute an expression over ranges inside SUM"},25+ {`=MAX(LEN(D1:D5))`, "6", "excelize doesn't apply LEN to each cell of a range inside MAX"},26+ {`=SEQUENCE(3, 2, 10)`, "10", "excelize has no SEQUENCE"},27+ {`=FILTER(D1:D5, C1:C5>3)`, "date", "excelize has no FILTER"},28+ {`=SORT(I1:I3)`, "10", "excelize has no SORT"},29+ {`=SORTN(C1:C7, 1, 0, 1, FALSE)`, "7", "excelize has no SORTN, which only Sheets has"},30+ {`=FLATTEN(C1:D2)`, "1", "excelize has no FLATTEN, which only Sheets has"},31+ {`=CHOOSECOLS(C1:E2, 2)`, "Apple", "excelize has no CHOOSECOLS"},32+ {`=CHOOSEROWS(C1:D3, -1)`, "3", "excelize has no CHOOSEROWS"},33+ {`=ARRAYFORMULA(SUM(C1:C3*G1:G3))`, "11", "excelize has no ARRAYFORMULA, which only Sheets has"},34+ {`=LET(x, C3, x*x)`, "9", "excelize has no LET"},35+ {`=LAMBDA(v, v+1)(C2)`, "3", "excelize has no LAMBDA"},36+ {`=REDUCE(0, C1:C5, LAMBDA(a, v, a+v))`, "15", "excelize has no REDUCE"},37+ {`=MAP(C1:C3, LAMBDA(v, v*v))`, "1", "excelize has no MAP"},38+ {`=REGEXEXTRACT("order 42", "[0-9]+")`, "42", "excelize has no REGEXEXTRACT"},39+ {`=REGEXREPLACE("a-b", "-", "+")`, "a+b", "excelize has no REGEXREPLACE"},40+ {`=REGEXMATCH("Cherry", "^Ch")`, "TRUE", "excelize has no REGEXMATCH, which only Sheets has"},41+ {`=SPLIT("x,y", ",")`, "x", "excelize has no SPLIT, which only Sheets has"},42+}43+44+func TestArraysAgainstExcelize(t *testing.T) {45+ s, x := build(t)46+ const cell = "Z1"47+ for _, tc := range arrayFormulas {48+ if err := s.Set(addr(cell), tc.formula); err != nil {49+ t.Errorf("012 rejects %s: %v", tc.formula, err)50+ continue51+ }52+ ours := s.Value(addr(cell))53+ s.Set(addr(cell), "")54+ if tc.why != "" {55+ t.Logf("skip %s: %s", tc.formula, tc.why)56+ if !agree(ours, tc.sheets) {57+ t.Errorf("%s: 012 %s, Sheets %s", tc.formula, show(ours), tc.sheets)58+ }59+ continue60+ }61+ if err := x.SetCellFormula("Sheet1", cell, toExcel(tc.formula)); err != nil {62+ t.Fatal(err)63+ }64+ theirs, err := x.CalcCellValue("Sheet1", cell, excelize.Options{RawCellValue: true})65+ if err != nil && !strings.HasPrefix(theirs, "#") {66+ theirs = err.Error()67+ }68+ if !agree(ours, theirs) {69+ t.Errorf("%s: 012 %s, excelize %q", tc.formula, show(ours), theirs)70+ }71+ }72+}
M oracle/intersect_test.go +6 -4
@@ -15,7 +15,9 @@ import (
1515 // other numbers. excelize reads the first cell of any range instead, so1616 // it agrees only where that is the intersection, or where a function1717 // takes the range; the other cases are skipped against excelize and give18-// what Excel and Sheets do (excel), which 012 is checked against.18+// what Excel and Sheets do (excel), which 012 is checked against. A19+// range that is a cell's whole formula spills in Sheets and Excel 365,20+// so a unary + makes it one value where the intersection is tested.1921 var intersections = []struct{ cell, formula, excel string }{2022 // The intersection is the first cell, or a function takes the range.2123 {"F2", "=B2:B4+1", ""}, {"F2", "=Rent*2", ""}, {"B8", "=B6:D6*3", ""},
@@ -24,9 +26,9 @@ var intersections = []struct{ cell, formula, excel string }{
2426 2527 // excelize reads the first cell where Excel and Sheets intersect.2628 {"F3", "=Rent*2", "2900"}, {"F4", "=Rent*2", "3000"}, {"F5", "=Rent*2", "#VALUE!"},27- {"G3", "=B2:B4", "1450"}, {"G4", "=ABS(-Rent)", "1500"}, {"G3", `=Rent&"x"`, "1450x"},28- {"C8", "=B6:D6*3", "60"}, {"E8", "=B6:D6*3", "#VALUE!"}, {"A9", "=B2:C4", "#VALUE!"},29- {"F3", "=Other!B2:B4*10", "20"}, {"C1", "=Other!B9:D9", "5"}, {"F1", "=Other!B2:B4", "#VALUE!"},29+ {"G3", "=+B2:B4", "1450"}, {"G4", "=ABS(-Rent)", "1500"}, {"G3", `=Rent&"x"`, "1450x"},30+ {"C8", "=B6:D6*3", "60"}, {"E8", "=B6:D6*3", "#VALUE!"}, {"A9", "=+B2:C4", "#VALUE!"},31+ {"F3", "=Other!B2:B4*10", "20"}, {"C1", "=+Other!B9:D9", "5"}, {"F1", "=+Other!B2:B4", "#VALUE!"},3032 }3133 3234 func TestImplicitIntersection(t *testing.T) {