=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>
| M | internal/ | 8 | +++- |
| M | internal/ | 6 | ++- |
| M | internal/ | 2 | +- |
| M | internal/ | 11 | +++-- |
| A | oracle/ | 72 | ++++++++++++++++++++++++++++++ |
| M | oracle/ | 10 | +++-- |
@@ -11,10 +11,14 @@ import (
1111 // and SORT of A:A work on its rows holding data, the blank rows past1212 // them being one fill (Array.Fill).1313 14+// arrayFormula is ARRAYFORMULA, which a range that is a cell's whole15+// formula is read through.16+var arrayFormula = &FuncDef{Name: "ARRAYFORMULA", Args: "array_formula", Desc: "Compute a formula over arrays: ranges read whole, and functions of one value applied to each entry", Min: 1, Max: 1,17+ eval: func(args []Node, get lookup) Value { return evalArr(args[0], get) }, arrays: takesArrays, format: inherit}18+1419 func init() {1520 define(16- &FuncDef{Name: "ARRAYFORMULA", Args: "array_formula", Desc: "Compute a formula over arrays: ranges read whole, and functions of one value applied to each entry", Min: 1, Max: 1,17- eval: func(args []Node, get lookup) Value { return evalArr(args[0], get) }, arrays: takesArrays, format: inherit},21+ arrayFormula,1822 &FuncDef{Name: "FILTER", Args: "range, condition1, [condition2, ...]", Desc: "The rows (or columns) of a range where every condition is true", Min: 2, Max: -1,1923 eval: filter, arrays: takesArrays, format: inheritFrom(0)},2024 &FuncDef{Name: "SORT", Args: "range, [sort_column], [is_ascending], [sort_column2, is_ascending2, ...]", Desc: "The rows of a range sorted by columns", Min: 1, Max: -1,@@ -25,8 +25,12 @@ func EvalAt(n Node, get *Reader, at Addr) Value {
2525 2626 // EvalCell computes the formula in the cell at, as EvalAt, and the array2727 // it computes when that is several values: the cell shows the first, and28-// the engine spills the array from it. A 1x1 array is one value.28+// the engine spills the array from it. A 1x1 array is one value. A range29+// that is the whole formula is an array, as Sheets spills =B2:B9.2930 func EvalCell(n Node, get *Reader, at Addr) (Value, *Array) {31+ if r, ok := n.(formula.Range); ok {32+ n = formula.Call{Fn: arrayFormula, Args: []Node{r}}33+ }3034 return evalTop(n, get, at, true)3135 }3236 @@ -55,7 +55,7 @@ func TestExplainError(t *testing.T) {
5555 "B6": `=VLOOKUP(99, A1:A2, 1, FALSE)`,5656 "B7": "=SQRT(-1)",5757 "B8": "=C8", "C8": "=B8",58- "B9": "=A1:A2",58+ "B9": "=-A1:A2",5959 "B10": "=IFERROR(B1, 0)",6060 "B11": "=AVERAGE(A3)",6161 "B12": "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 }@@ -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+}@@ -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) {