oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// cases.go is the case list and nothing else.
4//
5// Split out of main.go when that file reached the 300-line cap, and
6// along the right seam: this file is DATA — the formulas the corpus
7// asks LibreOffice about — and main.go is the program that asks. The
8// list grows every time a function family lands, so it is the half that
9// would otherwise push the cap again on the next slice.
10
11// cases is the full case list, in the same order and with the same cell
12// references as testdata/corpus/formulas.json — so a run replaces the
13// file's "want" values and nothing else, and the diff is purely a
14// statement about what LibreOffice computed.
15//
16// The cell references matter. Ten cases read real cells
17// (VLOOKUP/INDEX/MATCH, and the three reading a deliberately blank
18// cell), and they use A1:B3 with C1 blank — exactly the table
19// calc/corpus_test.go's corpusEnv and xlsx/oracle_excelize_test.go's
20// newOracleWorkbook set up. That is why writeODS puts each row's own
21// formula in column D rather than column A: column A is support data
22// here, not case data. An earlier version of this list put the table at
23// B1:C3 and the formulas in column A, which would have produced a corpus
24// whose lookup entries no consuming test could evaluate — corpusEnv's
25// B1:B3 are the English names, not the numbers, so every generated
26// lookup row would have failed against the harness meant to read it.
27//
28// A prior version of this list also stopped at 41, silently dropping 9
29// plain scalar cases with no structural excuse plus the entire
30// VLOOKUP/INDEX/MATCH family and three more cases that read a genuinely
31// blank cell. Caught by code review, not by this tool running
32// successfully, since nothing here checked its own output size against
33// the file it was about to overwrite; main now does.
34//
35// Keep this list at parity with testdata/corpus/formulas.json whenever a
36// new function family is added, so a real gencorpus run always produces
37// a corpus at least as large as the one it replaces.
38var cases = []corpusCase{
39 {"SUM", "SUM(1,2,3)"},
40 {"SUM", "SUM(1,2,3,4,5)"},
41 {"SUM", "SUM(-5,5)"},
42 {"SUM", "SUM(1.5,2.5)"},
43 {"AVERAGE", "AVERAGE(2,4,6)"},
44 {"AVERAGE", "AVERAGE(1,2,3,4)"},
45 {"COUNT", `COUNT(1,2,"a",TRUE)`},
46 {"COUNTA", `COUNTA(1,2,"a",TRUE)`},
47 {"MIN", "MIN(4,2,9,1)"},
48 {"MAX", "MAX(4,2,9,1)"},
49 {"ROUND", "ROUND(3.14159,2)"},
50 {"ROUND", "ROUND(2.5,0)"},
51 {"ROUND", "ROUND(-2.5,0)"},
52 {"ROUNDUP", "ROUNDUP(3.14159,2)"},
53 {"ROUNDUP", "ROUNDUP(3.001,0)"},
54 {"ROUNDDOWN", "ROUNDDOWN(3.99,0)"},
55 {"ROUNDDOWN", "ROUNDDOWN(-3.99,0)"},
56 {"ABS", "ABS(-7)"},
57 {"ABS", "ABS(7)"},
58 {"SQRT", "SQRT(16)"},
59 {"SQRT", "SQRT(2)"},
60 {"POWER", "POWER(2,10)"},
61 {"POWER", "POWER(9,0.5)"},
62 {"MOD", "MOD(10,3)"},
63 {"MOD", "MOD(-10,3)"},
64 {"MOD", "MOD(10,-3)"},
65 {"IF", `IF(TRUE,"yes","no")`},
66 {"IF", `IF(FALSE,"yes","no")`},
67 {"IF", `IF(FALSE,1/0,5)`},
68 {"IFS", "IFS(FALSE,1,TRUE,2,TRUE,3)"},
69 {"AND", "AND(TRUE,TRUE,TRUE)"},
70 {"AND", "AND(TRUE,FALSE)"},
71 {"OR", "OR(FALSE,FALSE,TRUE)"},
72 {"OR", "OR(FALSE,FALSE)"},
73 {"NOT", "NOT(TRUE)"},
74 {"NOT", "NOT(FALSE)"},
75 {"IFERROR", `IFERROR(1/0,"fallback")`},
76 {"IFERROR", `IFERROR(5,"fallback")`},
77 {"CONCAT", `CONCAT("foo","bar")`},
78 {"CONCAT", `CONCAT("a",1,"b")`},
79 {"LEFT", `LEFT("hello",3)`},
80 {"LEFT", `LEFT("hello")`},
81 {"RIGHT", `RIGHT("hello",3)`},
82 {"RIGHT", `RIGHT("hello")`},
83 {"MID", `MID("hello",2,3)`},
84 {"LEN", `LEN("hello")`},
85 {"LEN", `LEN("")`},
86 {"TRIM", `TRIM(" a b ")`},
87 {"UPPER", `UPPER("MixedCase")`},
88 {"LOWER", `LOWER("MixedCase")`},
89 {"VLOOKUP", "VLOOKUP(20,A1:B3,2,FALSE)"},
90 {"VLOOKUP", "VLOOKUP(25,A1:B3,2,TRUE)"},
91 {"VLOOKUP", "VLOOKUP(99,A1:B3,2,FALSE)"},
92 {"INDEX", "INDEX(A1:B3,2,2)"},
93 {"INDEX", "INDEX(A1:B3,3,1)"},
94 {"MATCH", "MATCH(20,A1:A3,0)"},
95 {"MATCH", "MATCH(25,A1:A3,1)"},
96 {"COUNT", "COUNT(A1,C1)"},
97 {"COUNTA", "COUNTA(A1,C1)"},
98 {"AVERAGE", "AVERAGE(A1,A2,A3)"},
99
100 // The conditional aggregates. A1:A3 hold 10/20/30 and B1:B3 their
101 // English names, so every case below is answered by the same small
102 // table the lookup family already uses — no new support data, and
103 // the cases stay readable next to their expected answers.
104 //
105 // The wildcard cases are back, and the story is worth keeping.
106 //
107 // They were removed on 2026-09-03 because LibreOffice answered 0 to
108 // every one of them, which was read as "its profile does not do
109 // Excel wildcards, so it cannot be the oracle here". The
110 // measurement was right and the diagnosis was wrong: wildcards are
111 // a per-DOCUMENT calculation setting, and ods.go was writing a file
112 // with no <table:calculation-settings> at all, so LibreOffice fell
113 // back to ODF's legacy default — regular expressions on, wildcards
114 // off — where "t*" is the regex t* and matches nothing in "ten".
115 // The generator was asking the wrong question and getting a correct
116 // answer to it. One element in ods.go, and the same binary gives
117 // Excel's answers. Caught by review, 2026-09-04.
118 //
119 // The lesson is the general one: when an oracle disagrees, check
120 // what you actually asked it before deciding it is unfit.
121 {"SUMIF", `SUMIF(A1:A3,">15")`},
122 {"SUMIF", `SUMIF(A1:A3,">=20")`},
123 {"SUMIF", `SUMIF(B1:B3,"ten",A1:A3)`},
124 {"SUMIF", `SUMIF(B1:B3,"t*",A1:A3)`},
125 {"COUNTIF", `COUNTIF(B1:B3,"t*")`},
126 {"COUNTIF", `COUNTIF(B1:B3,"t??")`},
127 {"COUNTIF", `COUNTIF(B1:B3,"t?n")`},
128 {"COUNTIF", `COUNTIF(B1:B3,"*y")`},
129 {"SUMIFS", `SUMIFS(A1:A3,B1:B3,"t*")`},
130 {"COUNTIFS", `COUNTIFS(A1:A3,">=20",B1:B3,"t*")`},
131 {"COUNTIF", `COUNTIF(A1:A3,">15")`},
132 {"COUNTIF", `COUNTIF(A1:A3,"<>20")`},
133 {"AVERAGEIF", `AVERAGEIF(A1:A3,">15")`},
134 {"AVERAGEIF", `AVERAGEIF(B1:B3,"<>ten",A1:A3)`},
135 {"SUMIFS", `SUMIFS(A1:A3,A1:A3,">10",A1:A3,"<30")`},
136 {"COUNTIFS", `COUNTIFS(A1:A3,">100")`},
137 {"AVERAGEIFS", `AVERAGEIFS(A1:A3,B1:B3,"<>ten")`},
138
139 // A numeric criterion over a range that is NOT all numbers. Added
140 // 2026-09-04 after review: the first version of the matcher said
141 // "only <> is true of a cell that cannot be compared numerically",
142 // which counts every blank as not-equal-to-20. C1:C3 are blank in
143 // this support table and B1:B3 are text, so A1:C3 is the range that
144 // separates the three cases, and every earlier case used A1:A3
145 // where all three cells are numbers and nothing could disagree.
146 //
147 // excelize cannot settle these: asked the same question two ways it
148 // answered 5 for A1:C3 (text counted, blanks not) and 0 for B1:B3
149 // (the same text, not counted). LibreOffice is the oracle here.
150 {"COUNTIF", `COUNTIF(A1:C3,"<>20")`},
151 {"COUNTIF", `COUNTIF(B1:B3,"<>20")`},
152 {"COUNTIF", `COUNTIF(A1:C3,">5")`},
153 {"COUNTIF", `COUNTIF(A1:C3,"<>")`},
154 {"COUNTIF", `COUNTIF(A1:C3,"=")`},
155 {"SUMIF", `SUMIF(A1:C3,"<>20")`},
156 {"AVERAGEIF", `AVERAGEIF(A1:C3,">5")`},
157 {"COUNTIF", `COUNTIF(A1:C3,"ten")`},
158
159 // Mismatched range SHAPES are deliberately NOT here. A1:A3 is 3x1
160 // and A1:C1 is 1x3, and LibreOffice answers Err:502 — a refusal,
161 // which is the right answer and which this generator cannot record
162 // (it treats an error as a sign the ODF translation is broken, and
163 // that check is worth more than this one case). Pinned instead as a
164 // direct assertion in calc/fn_cond_test.go, with excelize's #VALUE!
165 // as the cross-check.
166
167 // The IS* predicates. C1 is deliberately blank in the support data,
168 // which is what makes the ISBLANK pair meaningful: one over a real
169 // blank and one over a cell holding a number.
170 {"ISBLANK", "ISBLANK(C1)"},
171 {"ISBLANK", "ISBLANK(A1)"},
172 {"ISNUMBER", "ISNUMBER(A1)"},
173 {"ISNUMBER", "ISNUMBER(B1)"},
174 {"ISTEXT", "ISTEXT(B1)"},
175 {"ISTEXT", "ISTEXT(A1)"},
176 {"ISERROR", "ISERROR(1/0)"},
177 {"ISERROR", "ISERROR(A1)"},
178
179 // The date parts. Every case wraps its result in a plain arithmetic
180 // or comparison so LibreOffice exports a NUMBER rather than a
181 // formatted date string: DATE() alone comes back through
182 // `--convert-to csv` as whatever the locale's date format renders,
183 // which is a string this corpus cannot compare against a serial.
184 // YEAR/MONTH/DAY already return plain numbers.
185 {"YEAR", "YEAR(DATE(2026,9,3))"},
186 {"MONTH", "MONTH(DATE(2026,9,3))"},
187 {"DAY", "DAY(DATE(2026,9,3))"},
188 {"MONTH", "MONTH(DATE(2026,13,1))"},
189 {"YEAR", "YEAR(DATE(2026,13,1))"},
190 {"DAY", "DAY(DATE(2026,3,0))"},
191 {"MONTH", "MONTH(EOMONTH(DATE(2026,1,31),1))"},
192 {"DAY", "DAY(EOMONTH(DATE(2026,1,31),1))"},
193 {"DAY", "DAY(EOMONTH(DATE(2024,1,15),1))"},
194 {"YEAR", "YEAR(EOMONTH(DATE(2026,12,1),1))"},
195
196 // VALUE, SUBSTITUTE, FIND and SEARCH. FIND and SEARCH differ in
197 // exactly two ways and both are easy to get backwards, so each has
198 // a case that separates them: "T" against "ten" is 1 for SEARCH
199 // (case-insensitive) and not found for FIND, and the wildcard case
200 // only SEARCH accepts.
201 {"VALUE", `VALUE("42")`},
202 {"VALUE", `VALUE("3.5")`},
203 {"VALUE", `VALUE("50%")`},
204 {"SUBSTITUTE", `SUBSTITUTE("a-b-c","-","+")`},
205 {"SUBSTITUTE", `SUBSTITUTE("a-b-c","-","+",2)`},
206 {"SUBSTITUTE", `SUBSTITUTE("aaa","a","b",3)`},
207 {"FIND", `FIND("n","ten")`},
208 {"FIND", `FIND("e","seventeen",4)`},
209 {"SEARCH", `SEARCH("T","ten")`},
210 {"SEARCH", `SEARCH("e?e","seventeen")`},
211 {"LEN", `LEN(SUBSTITUTE("a-b-c","-",""))`},
212}
213