Total Complexity | 209 |
Total Lines | 1638 |
Duplicated Lines | 0 % |
Coverage | 89.55% |
Changes | 0 |
Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
35 | class Parser |
||
36 | { |
||
37 | /** Constants */ |
||
38 | // Sheet title in unquoted form |
||
39 | // Invalid sheet title characters cannot occur in the sheet title: |
||
40 | // *:/\?[] |
||
41 | // Moreover, there are valid sheet title characters that cannot occur in unquoted form (there may be more?) |
||
42 | // +-% '^&<>=,;#()"{} |
||
43 | const REGEX_SHEET_TITLE_UNQUOTED = '[^\*\:\/\\\\\?\[\]\+\-\% \\\'\^\&\<\>\=\,\;\#\(\)\"\{\}]+'; |
||
44 | |||
45 | // Sheet title in quoted form (without surrounding quotes) |
||
46 | // Invalid sheet title characters cannot occur in the sheet title: |
||
47 | // *:/\?[] (usual invalid sheet title characters) |
||
48 | // Single quote is represented as a pair '' |
||
49 | // Former value for this constant led to "catastrophic backtracking", |
||
50 | // unable to handle double apostrophes. |
||
51 | // (*COMMIT) should prevent this. |
||
52 | const REGEX_SHEET_TITLE_QUOTED = "([^*:/\\\\?\\[\\]']|'')+"; |
||
53 | |||
54 | const REGEX_CELL_TITLE_QUOTED = "~^'" |
||
55 | . self::REGEX_SHEET_TITLE_QUOTED |
||
56 | . '(:' . self::REGEX_SHEET_TITLE_QUOTED . ')?' |
||
57 | . "'!(*COMMIT)" |
||
58 | . '[$]?[A-Ia-i]?[A-Za-z][$]?(\d+)' |
||
59 | . '$~u'; |
||
60 | |||
61 | const REGEX_RANGE_TITLE_QUOTED = "~^'" |
||
62 | . self::REGEX_SHEET_TITLE_QUOTED |
||
63 | . '(:' . self::REGEX_SHEET_TITLE_QUOTED . ')?' |
||
64 | . "'!(*COMMIT)" |
||
65 | . '[$]?[A-Ia-i]?[A-Za-z][$]?(\d+)' |
||
66 | . ':' |
||
67 | . '[$]?[A-Ia-i]?[A-Za-z][$]?(\d+)' |
||
68 | . '$~u'; |
||
69 | |||
70 | private const UTF8 = 'UTF-8'; |
||
71 | |||
72 | /** |
||
73 | * The index of the character we are currently looking at. |
||
74 | */ |
||
75 | public int $currentCharacter; |
||
76 | |||
77 | /** |
||
78 | * The token we are working on. |
||
79 | */ |
||
80 | public string $currentToken; |
||
81 | |||
82 | /** |
||
83 | * The formula to parse. |
||
84 | */ |
||
85 | private string $formula; |
||
86 | |||
87 | /** |
||
88 | * The character ahead of the current char. |
||
89 | */ |
||
90 | public string $lookAhead; |
||
91 | |||
92 | /** |
||
93 | * The parse tree to be generated. |
||
94 | * |
||
95 | * @var mixed[]|string |
||
96 | */ |
||
97 | public array|string $parseTree; |
||
98 | |||
99 | /** |
||
100 | * Array of external sheets. |
||
101 | * |
||
102 | * @var array<string, int> |
||
103 | */ |
||
104 | private array $externalSheets; |
||
105 | |||
106 | /** |
||
107 | * Array of sheet references in the form of REF structures. |
||
108 | * |
||
109 | * @var array<int|string, int|string> |
||
110 | */ |
||
111 | public array $references; |
||
112 | |||
113 | /** |
||
114 | * The Excel ptg indices. |
||
115 | * |
||
116 | * @var array<string, int> |
||
117 | */ |
||
118 | private array $ptg = [ |
||
119 | 'ptgExp' => 0x01, |
||
120 | 'ptgTbl' => 0x02, |
||
121 | 'ptgAdd' => 0x03, |
||
122 | 'ptgSub' => 0x04, |
||
123 | 'ptgMul' => 0x05, |
||
124 | 'ptgDiv' => 0x06, |
||
125 | 'ptgPower' => 0x07, |
||
126 | 'ptgConcat' => 0x08, |
||
127 | 'ptgLT' => 0x09, |
||
128 | 'ptgLE' => 0x0A, |
||
129 | 'ptgEQ' => 0x0B, |
||
130 | 'ptgGE' => 0x0C, |
||
131 | 'ptgGT' => 0x0D, |
||
132 | 'ptgNE' => 0x0E, |
||
133 | 'ptgIsect' => 0x0F, |
||
134 | 'ptgUnion' => 0x10, |
||
135 | 'ptgRange' => 0x11, |
||
136 | 'ptgUplus' => 0x12, |
||
137 | 'ptgUminus' => 0x13, |
||
138 | 'ptgPercent' => 0x14, |
||
139 | 'ptgParen' => 0x15, |
||
140 | 'ptgMissArg' => 0x16, |
||
141 | 'ptgStr' => 0x17, |
||
142 | 'ptgAttr' => 0x19, |
||
143 | 'ptgSheet' => 0x1A, |
||
144 | 'ptgEndSheet' => 0x1B, |
||
145 | 'ptgErr' => 0x1C, |
||
146 | 'ptgBool' => 0x1D, |
||
147 | 'ptgInt' => 0x1E, |
||
148 | 'ptgNum' => 0x1F, |
||
149 | 'ptgArray' => 0x20, |
||
150 | 'ptgFunc' => 0x21, |
||
151 | 'ptgFuncVar' => 0x22, |
||
152 | 'ptgName' => 0x23, |
||
153 | 'ptgRef' => 0x24, |
||
154 | 'ptgArea' => 0x25, |
||
155 | 'ptgMemArea' => 0x26, |
||
156 | 'ptgMemErr' => 0x27, |
||
157 | 'ptgMemNoMem' => 0x28, |
||
158 | 'ptgMemFunc' => 0x29, |
||
159 | 'ptgRefErr' => 0x2A, |
||
160 | 'ptgAreaErr' => 0x2B, |
||
161 | 'ptgRefN' => 0x2C, |
||
162 | 'ptgAreaN' => 0x2D, |
||
163 | 'ptgMemAreaN' => 0x2E, |
||
164 | 'ptgMemNoMemN' => 0x2F, |
||
165 | 'ptgNameX' => 0x39, |
||
166 | 'ptgRef3d' => 0x3A, |
||
167 | 'ptgArea3d' => 0x3B, |
||
168 | 'ptgRefErr3d' => 0x3C, |
||
169 | 'ptgAreaErr3d' => 0x3D, |
||
170 | 'ptgArrayV' => 0x40, |
||
171 | 'ptgFuncV' => 0x41, |
||
172 | 'ptgFuncVarV' => 0x42, |
||
173 | 'ptgNameV' => 0x43, |
||
174 | 'ptgRefV' => 0x44, |
||
175 | 'ptgAreaV' => 0x45, |
||
176 | 'ptgMemAreaV' => 0x46, |
||
177 | 'ptgMemErrV' => 0x47, |
||
178 | 'ptgMemNoMemV' => 0x48, |
||
179 | 'ptgMemFuncV' => 0x49, |
||
180 | 'ptgRefErrV' => 0x4A, |
||
181 | 'ptgAreaErrV' => 0x4B, |
||
182 | 'ptgRefNV' => 0x4C, |
||
183 | 'ptgAreaNV' => 0x4D, |
||
184 | 'ptgMemAreaNV' => 0x4E, |
||
185 | 'ptgMemNoMemNV' => 0x4F, |
||
186 | 'ptgFuncCEV' => 0x58, |
||
187 | 'ptgNameXV' => 0x59, |
||
188 | 'ptgRef3dV' => 0x5A, |
||
189 | 'ptgArea3dV' => 0x5B, |
||
190 | 'ptgRefErr3dV' => 0x5C, |
||
191 | 'ptgAreaErr3dV' => 0x5D, |
||
192 | 'ptgArrayA' => 0x60, |
||
193 | 'ptgFuncA' => 0x61, |
||
194 | 'ptgFuncVarA' => 0x62, |
||
195 | 'ptgNameA' => 0x63, |
||
196 | 'ptgRefA' => 0x64, |
||
197 | 'ptgAreaA' => 0x65, |
||
198 | 'ptgMemAreaA' => 0x66, |
||
199 | 'ptgMemErrA' => 0x67, |
||
200 | 'ptgMemNoMemA' => 0x68, |
||
201 | 'ptgMemFuncA' => 0x69, |
||
202 | 'ptgRefErrA' => 0x6A, |
||
203 | 'ptgAreaErrA' => 0x6B, |
||
204 | 'ptgRefNA' => 0x6C, |
||
205 | 'ptgAreaNA' => 0x6D, |
||
206 | 'ptgMemAreaNA' => 0x6E, |
||
207 | 'ptgMemNoMemNA' => 0x6F, |
||
208 | 'ptgFuncCEA' => 0x78, |
||
209 | 'ptgNameXA' => 0x79, |
||
210 | 'ptgRef3dA' => 0x7A, |
||
211 | 'ptgArea3dA' => 0x7B, |
||
212 | 'ptgRefErr3dA' => 0x7C, |
||
213 | 'ptgAreaErr3dA' => 0x7D, |
||
214 | ]; |
||
215 | |||
216 | /** |
||
217 | * Thanks to Michael Meeks and Gnumeric for the initial arg values. |
||
218 | * |
||
219 | * The following hash was generated by "function_locale.pl" in the distro. |
||
220 | * Refer to function_locale.pl for non-English function names. |
||
221 | * |
||
222 | * The array elements are as follow: |
||
223 | * ptg: The Excel function ptg code. |
||
224 | * args: The number of arguments that the function takes: |
||
225 | * >=0 is a fixed number of arguments. |
||
226 | * -1 is a variable number of arguments. |
||
227 | * class: The reference, value or array class of the function args. |
||
228 | * vol: The function is volatile. |
||
229 | * |
||
230 | * @var array<string, array{int, int, int, int}> |
||
1 ignored issue
–
show
|
|||
231 | */ |
||
232 | private array $functions = [ |
||
233 | // function ptg args class vol |
||
234 | 'COUNT' => [0, -1, 0, 0], |
||
235 | 'IF' => [1, -1, 1, 0], |
||
236 | 'ISNA' => [2, 1, 1, 0], |
||
237 | 'ISERROR' => [3, 1, 1, 0], |
||
238 | 'SUM' => [4, -1, 0, 0], |
||
239 | 'AVERAGE' => [5, -1, 0, 0], |
||
240 | 'MIN' => [6, -1, 0, 0], |
||
241 | 'MAX' => [7, -1, 0, 0], |
||
242 | 'ROW' => [8, -1, 0, 0], |
||
243 | 'COLUMN' => [9, -1, 0, 0], |
||
244 | 'NA' => [10, 0, 0, 0], |
||
245 | 'NPV' => [11, -1, 1, 0], |
||
246 | 'STDEV' => [12, -1, 0, 0], |
||
247 | 'DOLLAR' => [13, -1, 1, 0], |
||
248 | 'FIXED' => [14, -1, 1, 0], |
||
249 | 'SIN' => [15, 1, 1, 0], |
||
250 | 'COS' => [16, 1, 1, 0], |
||
251 | 'TAN' => [17, 1, 1, 0], |
||
252 | 'ATAN' => [18, 1, 1, 0], |
||
253 | 'PI' => [19, 0, 1, 0], |
||
254 | 'SQRT' => [20, 1, 1, 0], |
||
255 | 'EXP' => [21, 1, 1, 0], |
||
256 | 'LN' => [22, 1, 1, 0], |
||
257 | 'LOG10' => [23, 1, 1, 0], |
||
258 | 'ABS' => [24, 1, 1, 0], |
||
259 | 'INT' => [25, 1, 1, 0], |
||
260 | 'SIGN' => [26, 1, 1, 0], |
||
261 | 'ROUND' => [27, 2, 1, 0], |
||
262 | 'LOOKUP' => [28, -1, 0, 0], |
||
263 | 'INDEX' => [29, -1, 0, 1], |
||
264 | 'REPT' => [30, 2, 1, 0], |
||
265 | 'MID' => [31, 3, 1, 0], |
||
266 | 'LEN' => [32, 1, 1, 0], |
||
267 | 'VALUE' => [33, 1, 1, 0], |
||
268 | 'TRUE' => [34, 0, 1, 0], |
||
269 | 'FALSE' => [35, 0, 1, 0], |
||
270 | 'AND' => [36, -1, 0, 0], |
||
271 | 'OR' => [37, -1, 0, 0], |
||
272 | 'NOT' => [38, 1, 1, 0], |
||
273 | 'MOD' => [39, 2, 1, 0], |
||
274 | 'DCOUNT' => [40, 3, 0, 0], |
||
275 | 'DSUM' => [41, 3, 0, 0], |
||
276 | 'DAVERAGE' => [42, 3, 0, 0], |
||
277 | 'DMIN' => [43, 3, 0, 0], |
||
278 | 'DMAX' => [44, 3, 0, 0], |
||
279 | 'DSTDEV' => [45, 3, 0, 0], |
||
280 | 'VAR' => [46, -1, 0, 0], |
||
281 | 'DVAR' => [47, 3, 0, 0], |
||
282 | 'TEXT' => [48, 2, 1, 0], |
||
283 | 'LINEST' => [49, -1, 0, 0], |
||
284 | 'TREND' => [50, -1, 0, 0], |
||
285 | 'LOGEST' => [51, -1, 0, 0], |
||
286 | 'GROWTH' => [52, -1, 0, 0], |
||
287 | 'PV' => [56, -1, 1, 0], |
||
288 | 'FV' => [57, -1, 1, 0], |
||
289 | 'NPER' => [58, -1, 1, 0], |
||
290 | 'PMT' => [59, -1, 1, 0], |
||
291 | 'RATE' => [60, -1, 1, 0], |
||
292 | 'MIRR' => [61, 3, 0, 0], |
||
293 | 'IRR' => [62, -1, 0, 0], |
||
294 | 'RAND' => [63, 0, 1, 1], |
||
295 | 'MATCH' => [64, -1, 0, 0], |
||
296 | 'DATE' => [65, 3, 1, 0], |
||
297 | 'TIME' => [66, 3, 1, 0], |
||
298 | 'DAY' => [67, 1, 1, 0], |
||
299 | 'MONTH' => [68, 1, 1, 0], |
||
300 | 'YEAR' => [69, 1, 1, 0], |
||
301 | 'WEEKDAY' => [70, -1, 1, 0], |
||
302 | 'HOUR' => [71, 1, 1, 0], |
||
303 | 'MINUTE' => [72, 1, 1, 0], |
||
304 | 'SECOND' => [73, 1, 1, 0], |
||
305 | 'NOW' => [74, 0, 1, 1], |
||
306 | 'AREAS' => [75, 1, 0, 1], |
||
307 | 'ROWS' => [76, 1, 0, 1], |
||
308 | 'COLUMNS' => [77, 1, 0, 1], |
||
309 | 'OFFSET' => [78, -1, 0, 1], |
||
310 | 'SEARCH' => [82, -1, 1, 0], |
||
311 | 'TRANSPOSE' => [83, 1, 1, 0], |
||
312 | 'TYPE' => [86, 1, 1, 0], |
||
313 | 'ATAN2' => [97, 2, 1, 0], |
||
314 | 'ASIN' => [98, 1, 1, 0], |
||
315 | 'ACOS' => [99, 1, 1, 0], |
||
316 | 'CHOOSE' => [100, -1, 1, 0], |
||
317 | 'HLOOKUP' => [101, -1, 0, 0], |
||
318 | 'VLOOKUP' => [102, -1, 0, 0], |
||
319 | 'ISREF' => [105, 1, 0, 0], |
||
320 | 'LOG' => [109, -1, 1, 0], |
||
321 | 'CHAR' => [111, 1, 1, 0], |
||
322 | 'LOWER' => [112, 1, 1, 0], |
||
323 | 'UPPER' => [113, 1, 1, 0], |
||
324 | 'PROPER' => [114, 1, 1, 0], |
||
325 | 'LEFT' => [115, -1, 1, 0], |
||
326 | 'RIGHT' => [116, -1, 1, 0], |
||
327 | 'EXACT' => [117, 2, 1, 0], |
||
328 | 'TRIM' => [118, 1, 1, 0], |
||
329 | 'REPLACE' => [119, 4, 1, 0], |
||
330 | 'SUBSTITUTE' => [120, -1, 1, 0], |
||
331 | 'CODE' => [121, 1, 1, 0], |
||
332 | 'FIND' => [124, -1, 1, 0], |
||
333 | 'CELL' => [125, -1, 0, 1], |
||
334 | 'ISERR' => [126, 1, 1, 0], |
||
335 | 'ISTEXT' => [127, 1, 1, 0], |
||
336 | 'ISNUMBER' => [128, 1, 1, 0], |
||
337 | 'ISBLANK' => [129, 1, 1, 0], |
||
338 | 'T' => [130, 1, 0, 0], |
||
339 | 'N' => [131, 1, 0, 0], |
||
340 | 'DATEVALUE' => [140, 1, 1, 0], |
||
341 | 'TIMEVALUE' => [141, 1, 1, 0], |
||
342 | 'SLN' => [142, 3, 1, 0], |
||
343 | 'SYD' => [143, 4, 1, 0], |
||
344 | 'DDB' => [144, -1, 1, 0], |
||
345 | 'INDIRECT' => [148, -1, 1, 1], |
||
346 | 'CALL' => [150, -1, 1, 0], |
||
347 | 'CLEAN' => [162, 1, 1, 0], |
||
348 | 'MDETERM' => [163, 1, 2, 0], |
||
349 | 'MINVERSE' => [164, 1, 2, 0], |
||
350 | 'MMULT' => [165, 2, 2, 0], |
||
351 | 'IPMT' => [167, -1, 1, 0], |
||
352 | 'PPMT' => [168, -1, 1, 0], |
||
353 | 'COUNTA' => [169, -1, 0, 0], |
||
354 | 'PRODUCT' => [183, -1, 0, 0], |
||
355 | 'FACT' => [184, 1, 1, 0], |
||
356 | 'DPRODUCT' => [189, 3, 0, 0], |
||
357 | 'ISNONTEXT' => [190, 1, 1, 0], |
||
358 | 'STDEVP' => [193, -1, 0, 0], |
||
359 | 'VARP' => [194, -1, 0, 0], |
||
360 | 'DSTDEVP' => [195, 3, 0, 0], |
||
361 | 'DVARP' => [196, 3, 0, 0], |
||
362 | 'TRUNC' => [197, -1, 1, 0], |
||
363 | 'ISLOGICAL' => [198, 1, 1, 0], |
||
364 | 'DCOUNTA' => [199, 3, 0, 0], |
||
365 | 'USDOLLAR' => [204, -1, 1, 0], |
||
366 | 'FINDB' => [205, -1, 1, 0], |
||
367 | 'SEARCHB' => [206, -1, 1, 0], |
||
368 | 'REPLACEB' => [207, 4, 1, 0], |
||
369 | 'LEFTB' => [208, -1, 1, 0], |
||
370 | 'RIGHTB' => [209, -1, 1, 0], |
||
371 | 'MIDB' => [210, 3, 1, 0], |
||
372 | 'LENB' => [211, 1, 1, 0], |
||
373 | 'ROUNDUP' => [212, 2, 1, 0], |
||
374 | 'ROUNDDOWN' => [213, 2, 1, 0], |
||
375 | 'ASC' => [214, 1, 1, 0], |
||
376 | 'DBCS' => [215, 1, 1, 0], |
||
377 | 'RANK' => [216, -1, 0, 0], |
||
378 | 'ADDRESS' => [219, -1, 1, 0], |
||
379 | 'DAYS360' => [220, -1, 1, 0], |
||
380 | 'TODAY' => [221, 0, 1, 1], |
||
381 | 'VDB' => [222, -1, 1, 0], |
||
382 | 'MEDIAN' => [227, -1, 0, 0], |
||
383 | 'SUMPRODUCT' => [228, -1, 2, 0], |
||
384 | 'SINH' => [229, 1, 1, 0], |
||
385 | 'COSH' => [230, 1, 1, 0], |
||
386 | 'TANH' => [231, 1, 1, 0], |
||
387 | 'ASINH' => [232, 1, 1, 0], |
||
388 | 'ACOSH' => [233, 1, 1, 0], |
||
389 | 'ATANH' => [234, 1, 1, 0], |
||
390 | 'DGET' => [235, 3, 0, 0], |
||
391 | 'INFO' => [244, 1, 1, 1], |
||
392 | 'DB' => [247, -1, 1, 0], |
||
393 | 'FREQUENCY' => [252, 2, 0, 0], |
||
394 | 'ERROR.TYPE' => [261, 1, 1, 0], |
||
395 | 'REGISTER.ID' => [267, -1, 1, 0], |
||
396 | 'AVEDEV' => [269, -1, 0, 0], |
||
397 | 'BETADIST' => [270, -1, 1, 0], |
||
398 | 'GAMMALN' => [271, 1, 1, 0], |
||
399 | 'BETAINV' => [272, -1, 1, 0], |
||
400 | 'BINOMDIST' => [273, 4, 1, 0], |
||
401 | 'CHIDIST' => [274, 2, 1, 0], |
||
402 | 'CHIINV' => [275, 2, 1, 0], |
||
403 | 'COMBIN' => [276, 2, 1, 0], |
||
404 | 'CONFIDENCE' => [277, 3, 1, 0], |
||
405 | 'CRITBINOM' => [278, 3, 1, 0], |
||
406 | 'EVEN' => [279, 1, 1, 0], |
||
407 | 'EXPONDIST' => [280, 3, 1, 0], |
||
408 | 'FDIST' => [281, 3, 1, 0], |
||
409 | 'FINV' => [282, 3, 1, 0], |
||
410 | 'FISHER' => [283, 1, 1, 0], |
||
411 | 'FISHERINV' => [284, 1, 1, 0], |
||
412 | 'FLOOR' => [285, 2, 1, 0], |
||
413 | 'GAMMADIST' => [286, 4, 1, 0], |
||
414 | 'GAMMAINV' => [287, 3, 1, 0], |
||
415 | 'CEILING' => [288, 2, 1, 0], |
||
416 | 'HYPGEOMDIST' => [289, 4, 1, 0], |
||
417 | 'LOGNORMDIST' => [290, 3, 1, 0], |
||
418 | 'LOGINV' => [291, 3, 1, 0], |
||
419 | 'NEGBINOMDIST' => [292, 3, 1, 0], |
||
420 | 'NORMDIST' => [293, 4, 1, 0], |
||
421 | 'NORMSDIST' => [294, 1, 1, 0], |
||
422 | 'NORMINV' => [295, 3, 1, 0], |
||
423 | 'NORMSINV' => [296, 1, 1, 0], |
||
424 | 'STANDARDIZE' => [297, 3, 1, 0], |
||
425 | 'ODD' => [298, 1, 1, 0], |
||
426 | 'PERMUT' => [299, 2, 1, 0], |
||
427 | 'POISSON' => [300, 3, 1, 0], |
||
428 | 'TDIST' => [301, 3, 1, 0], |
||
429 | 'WEIBULL' => [302, 4, 1, 0], |
||
430 | 'SUMXMY2' => [303, 2, 2, 0], |
||
431 | 'SUMX2MY2' => [304, 2, 2, 0], |
||
432 | 'SUMX2PY2' => [305, 2, 2, 0], |
||
433 | 'CHITEST' => [306, 2, 2, 0], |
||
434 | 'CORREL' => [307, 2, 2, 0], |
||
435 | 'COVAR' => [308, 2, 2, 0], |
||
436 | 'FORECAST' => [309, 3, 2, 0], |
||
437 | 'FTEST' => [310, 2, 2, 0], |
||
438 | 'INTERCEPT' => [311, 2, 2, 0], |
||
439 | 'PEARSON' => [312, 2, 2, 0], |
||
440 | 'RSQ' => [313, 2, 2, 0], |
||
441 | 'STEYX' => [314, 2, 2, 0], |
||
442 | 'SLOPE' => [315, 2, 2, 0], |
||
443 | 'TTEST' => [316, 4, 2, 0], |
||
444 | 'PROB' => [317, -1, 2, 0], |
||
445 | 'DEVSQ' => [318, -1, 0, 0], |
||
446 | 'GEOMEAN' => [319, -1, 0, 0], |
||
447 | 'HARMEAN' => [320, -1, 0, 0], |
||
448 | 'SUMSQ' => [321, -1, 0, 0], |
||
449 | 'KURT' => [322, -1, 0, 0], |
||
450 | 'SKEW' => [323, -1, 0, 0], |
||
451 | 'ZTEST' => [324, -1, 0, 0], |
||
452 | 'LARGE' => [325, 2, 0, 0], |
||
453 | 'SMALL' => [326, 2, 0, 0], |
||
454 | 'QUARTILE' => [327, 2, 0, 0], |
||
455 | 'PERCENTILE' => [328, 2, 0, 0], |
||
456 | 'PERCENTRANK' => [329, -1, 0, 0], |
||
457 | 'MODE' => [330, -1, 2, 0], |
||
458 | 'TRIMMEAN' => [331, 2, 0, 0], |
||
459 | 'TINV' => [332, 2, 1, 0], |
||
460 | 'CONCATENATE' => [336, -1, 1, 0], |
||
461 | 'POWER' => [337, 2, 1, 0], |
||
462 | 'RADIANS' => [342, 1, 1, 0], |
||
463 | 'DEGREES' => [343, 1, 1, 0], |
||
464 | 'SUBTOTAL' => [344, -1, 0, 0], |
||
465 | 'SUMIF' => [345, -1, 0, 0], |
||
466 | 'COUNTIF' => [346, 2, 0, 0], |
||
467 | 'COUNTBLANK' => [347, 1, 0, 0], |
||
468 | 'ISPMT' => [350, 4, 1, 0], |
||
469 | 'DATEDIF' => [351, 3, 1, 0], |
||
470 | 'DATESTRING' => [352, 1, 1, 0], |
||
471 | 'NUMBERSTRING' => [353, 2, 1, 0], |
||
472 | 'ROMAN' => [354, -1, 1, 0], |
||
473 | 'GETPIVOTDATA' => [358, -1, 0, 0], |
||
474 | 'HYPERLINK' => [359, -1, 1, 0], |
||
475 | 'PHONETIC' => [360, 1, 0, 0], |
||
476 | 'AVERAGEA' => [361, -1, 0, 0], |
||
477 | 'MAXA' => [362, -1, 0, 0], |
||
478 | 'MINA' => [363, -1, 0, 0], |
||
479 | 'STDEVPA' => [364, -1, 0, 0], |
||
480 | 'VARPA' => [365, -1, 0, 0], |
||
481 | 120 | 'STDEVA' => [366, -1, 0, 0], |
|
482 | 'VARA' => [367, -1, 0, 0], |
||
483 | 120 | 'BAHTTEXT' => [368, 1, 0, 0], |
|
484 | ]; |
||
485 | 120 | ||
486 | 120 | private Spreadsheet $spreadsheet; |
|
487 | 120 | ||
488 | 120 | /** |
|
489 | 120 | * The class constructor. |
|
490 | 120 | */ |
|
491 | 120 | public function __construct(Spreadsheet $spreadsheet) |
|
492 | { |
||
493 | $this->spreadsheet = $spreadsheet; |
||
494 | |||
495 | $this->currentCharacter = 0; |
||
496 | $this->currentToken = ''; // The token we are working on. |
||
497 | $this->formula = ''; // The formula to parse. |
||
498 | $this->lookAhead = ''; // The character ahead of the current char. |
||
499 | $this->parseTree = ''; // The parse tree to be generated. |
||
500 | $this->externalSheets = []; |
||
501 | 53 | $this->references = []; |
|
502 | } |
||
503 | 53 | ||
504 | 24 | /** |
|
505 | * Convert a token to the proper ptg value. |
||
506 | 52 | * |
|
507 | 45 | * @param string $token the token to convert |
|
508 | * |
||
509 | * @return string the converted token on success |
||
510 | 50 | */ |
|
511 | 30 | private function convert(string $token): string |
|
512 | { |
||
513 | if (Preg::isMatch('/"([^"]|""){0,255}"/', $token)) { |
||
514 | 50 | return $this->convertString($token); |
|
515 | 1 | } |
|
516 | if (is_numeric($token)) { |
||
517 | return $this->convertNumber($token); |
||
518 | 50 | } |
|
519 | 8 | // match references like A1 or $A$1 |
|
520 | if (Preg::isMatch('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) { |
||
521 | return $this->convertRef2d($token); |
||
522 | 48 | } |
|
523 | 26 | // match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1 |
|
524 | if (Preg::isMatch('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\!\$?[A-Ia-i]?[A-Za-z]\$?(\d+)$/u', $token)) { |
||
525 | return $this->convertRef3d($token); |
||
526 | 47 | } |
|
527 | // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1 |
||
528 | if (self::matchCellSheetnameQuoted($token)) { |
||
529 | return $this->convertRef3d($token); |
||
530 | 47 | } |
|
531 | 4 | // match ranges like A1:B2 or $A$1:$B$2 |
|
532 | if (Preg::isMatch('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) { |
||
533 | return $this->convertRange2d($token); |
||
534 | 47 | } |
|
535 | 29 | // match external ranges like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 |
|
536 | if (Preg::isMatch('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\!\$?([A-Ia-i]?[A-Za-z])?\$?(\d+)\:\$?([A-Ia-i]?[A-Za-z])?\$?(\d+)$/u', $token)) { |
||
537 | return $this->convertRange3d($token); |
||
538 | 38 | } |
|
539 | 1 | // match external ranges like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 |
|
540 | if (self::matchRangeSheetnameQuoted($token)) { |
||
541 | 38 | return $this->convertRange3d($token); |
|
542 | 6 | } |
|
543 | // operators (including parentheses) |
||
544 | if (isset($this->ptg[$token])) { |
||
545 | return pack('C', $this->ptg[$token]); |
||
546 | } |
||
547 | // match error codes |
||
548 | if (Preg::isMatch('/^#[A-Z0\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') { |
||
549 | return $this->convertError($token); |
||
550 | 35 | } |
|
551 | 34 | if (Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) { |
|
552 | return $this->convertDefinedName($token); |
||
553 | 3 | } |
|
554 | 2 | // commented so argument number can be processed correctly. See toReversePolish(). |
|
555 | /*if (Preg::isMatch("/[A-Z0-9\xc0-\xdc\.]+/", $token)) |
||
556 | 3 | { |
|
557 | 2 | return($this->convertFunction($token, $this->_func_args)); |
|
558 | }*/ |
||
559 | // if it's an argument, ignore the token (the argument remains) |
||
560 | if ($token == 'arg') { |
||
561 | 1 | return ''; |
|
562 | } |
||
563 | if (Preg::isMatch('/^true$/i', $token)) { |
||
564 | return $this->convertBool(1); |
||
565 | } |
||
566 | if (Preg::isMatch('/^false$/i', $token)) { |
||
567 | return $this->convertBool(0); |
||
568 | } |
||
569 | 45 | ||
570 | // TODO: use real error codes |
||
571 | throw new WriterException("Unknown token $token"); |
||
572 | 45 | } |
|
573 | 45 | ||
574 | /** |
||
575 | * Convert a number token to ptgInt or ptgNum. |
||
576 | * |
||
577 | 12 | * @param float|int|string $num an integer or double for conversion to its ptg value |
|
578 | */ |
||
579 | private function convertNumber(mixed $num): string |
||
580 | { |
||
581 | 12 | // Integer in the range 0..2**16-1 |
|
582 | if ((Preg::isMatch('/^\d+$/', (string) $num)) && ($num <= 65535)) { |
||
583 | return pack('Cv', $this->ptg['ptgInt'], $num); |
||
584 | 2 | } |
|
585 | |||
586 | 2 | // A float |
|
587 | if (BIFFwriter::getByteOrder()) { // if it's Big Endian |
||
588 | $num = strrev((string) $num); |
||
589 | } |
||
590 | |||
591 | return pack('Cd', $this->ptg['ptgNum'], $num); |
||
592 | } |
||
593 | |||
594 | private function convertBool(int $num): string |
||
595 | { |
||
596 | 24 | return pack('CC', $this->ptg['ptgBool'], $num); |
|
597 | } |
||
598 | |||
599 | 24 | /** |
|
600 | 24 | * Convert a string token to ptgStr. |
|
601 | * |
||
602 | * @param string $string a string for conversion to its ptg value |
||
603 | * |
||
604 | 24 | * @return string the converted token |
|
605 | */ |
||
606 | private function convertString(string $string): string |
||
607 | { |
||
608 | // chop away beggining and ending quotes |
||
609 | $string = substr($string, 1, -1); |
||
610 | if (strlen($string) > 255) { |
||
611 | throw new WriterException('String is too long'); |
||
612 | } |
||
613 | |||
614 | return pack('C', $this->ptg['ptgStr']) . StringHelper::UTF8toBIFF8UnicodeShort($string); |
||
615 | } |
||
616 | 33 | ||
617 | /** |
||
618 | 33 | * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of |
|
619 | * args that it takes. |
||
620 | * |
||
621 | 33 | * @param string $token the name of the function for convertion to ptg value |
|
622 | 14 | * @param int $num_args the number of arguments the function receives |
|
623 | * |
||
624 | * @return string The packed ptg for the function |
||
625 | */ |
||
626 | 30 | private function convertFunction(string $token, int $num_args): string |
|
627 | { |
||
628 | $args = $this->functions[$token][1]; |
||
629 | |||
630 | // Fixed number of args eg. TIME($i, $j, $k). |
||
631 | if ($args >= 0) { |
||
632 | return pack('Cv', $this->ptg['ptgFuncV'], $this->functions[$token][0]); |
||
633 | } |
||
634 | 26 | ||
635 | // Variable number of args eg. SUM($i, $j, $k, ..). |
||
636 | return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]); |
||
637 | } |
||
638 | 26 | ||
639 | 26 | /** |
|
640 | * Convert an Excel range such as A1:D4 to a ptgRefV. |
||
641 | * |
||
642 | * @param string $range An Excel range in the A1:A2 |
||
643 | */ |
||
644 | private function convertRange2d(string $range, int $class = 0): string |
||
645 | 26 | { |
|
646 | 26 | // TODO: possible class value 0,1,2 check Formula.pm |
|
647 | // Split the range into 2 cell refs |
||
648 | if (Preg::isMatch('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) { |
||
649 | 26 | [$cell1, $cell2] = explode(':', $range); |
|
650 | 26 | } else { |
|
651 | // TODO: use real error codes |
||
652 | throw new WriterException('Unknown range separator'); |
||
653 | } |
||
654 | // Convert the cell references |
||
655 | [$row1, $col1] = $this->cellToPackedRowcol($cell1); |
||
656 | [$row2, $col2] = $this->cellToPackedRowcol($cell2); |
||
657 | |||
658 | // The ptg value depends on the class of the ptg. |
||
659 | if ($class == 0) { |
||
660 | 26 | $ptgArea = pack('C', $this->ptg['ptgArea']); |
|
661 | } elseif ($class == 1) { |
||
662 | $ptgArea = pack('C', $this->ptg['ptgAreaV']); |
||
663 | } elseif ($class == 2) { |
||
664 | $ptgArea = pack('C', $this->ptg['ptgAreaA']); |
||
665 | } else { |
||
666 | // TODO: use real error codes |
||
667 | throw new WriterException("Unknown class $class"); |
||
668 | } |
||
669 | |||
670 | return $ptgArea . $row1 . $row2 . $col1 . $col2; |
||
671 | 4 | } |
|
672 | |||
673 | /** |
||
674 | 4 | * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to |
|
675 | * a ptgArea3d. |
||
676 | * |
||
677 | 4 | * @param string $token an Excel range in the Sheet1!A1:A2 format |
|
678 | * |
||
679 | * @return string the packed ptgArea3d token on success |
||
680 | 4 | */ |
|
681 | private function convertRange3d(string $token): string |
||
682 | { |
||
683 | 4 | // Split the ref at the ! symbol |
|
684 | 4 | [$ext_ref, $range] = PhpspreadsheetWorksheet::extractSheetTitle($token, true, true); |
|
685 | 4 | ||
686 | // Convert the external reference part (different for BIFF8) |
||
687 | $ext_ref = $this->getRefIndex($ext_ref ?? ''); |
||
688 | |||
689 | // Split the range into 2 cell refs |
||
690 | [$cell1, $cell2] = explode(':', $range ?? ''); |
||
691 | 4 | ||
692 | // Convert the cell references |
||
693 | 4 | if (Preg::isMatch('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $cell1)) { |
|
694 | [$row1, $col1] = $this->cellToPackedRowcol($cell1); |
||
695 | [$row2, $col2] = $this->cellToPackedRowcol($cell2); |
||
696 | } else { // It's a rows range (like 26:27) |
||
697 | [$row1, $col1, $row2, $col2] = $this->rangeToPackedRange($cell1 . ':' . $cell2); |
||
698 | } |
||
699 | |||
700 | // The ptg value depends on the class of the ptg. |
||
701 | $ptgArea = pack('C', $this->ptg['ptgArea3d']); |
||
702 | |||
703 | 30 | return $ptgArea . $ext_ref . $row1 . $row2 . $col1 . $col2; |
|
704 | } |
||
705 | |||
706 | 30 | /** |
|
707 | 30 | * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV. |
|
708 | * |
||
709 | * @param string $cell An Excel cell reference |
||
710 | 30 | * |
|
711 | * @return string The cell in packed() format with the corresponding ptg |
||
712 | 30 | */ |
|
713 | private function convertRef2d(string $cell): string |
||
714 | { |
||
715 | // Convert the cell reference |
||
716 | $cell_array = $this->cellToPackedRowcol($cell); |
||
717 | [$row, $col] = $cell_array; |
||
718 | |||
719 | // The ptg value depends on the class of the ptg. |
||
720 | $ptgRef = pack('C', $this->ptg['ptgRefA']); |
||
721 | |||
722 | return $ptgRef . $row . $col; |
||
723 | 8 | } |
|
724 | |||
725 | /** |
||
726 | 8 | * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a |
|
727 | * ptgRef3d. |
||
728 | * |
||
729 | 8 | * @param string $cell An Excel cell reference |
|
730 | * |
||
731 | * @return string the packed ptgRef3d token on success |
||
732 | 7 | */ |
|
733 | private function convertRef3d(string $cell): string |
||
734 | { |
||
735 | 7 | // Split the ref at the ! symbol |
|
736 | [$ext_ref, $cell] = PhpspreadsheetWorksheet::extractSheetTitle($cell, true, true); |
||
737 | 7 | ||
738 | // Convert the external reference part (different for BIFF8) |
||
739 | $ext_ref = $this->getRefIndex($ext_ref ?? ''); |
||
740 | |||
741 | // Convert the cell reference part |
||
742 | [$row, $col] = $this->cellToPackedRowcol($cell ?? ''); |
||
743 | |||
744 | // The ptg value depends on the class of the ptg. |
||
745 | $ptgRef = pack('C', $this->ptg['ptgRef3dA']); |
||
746 | |||
747 | 1 | return $ptgRef . $ext_ref . $row . $col; |
|
748 | } |
||
749 | 1 | ||
750 | /** |
||
751 | * Convert an error code to a ptgErr. |
||
752 | * |
||
753 | * @param string $errorCode The error code for conversion to its ptg value |
||
754 | 1 | * |
|
755 | * @return string The error code ptgErr |
||
756 | */ |
||
757 | 1 | private function convertError(string $errorCode): string |
|
758 | 1 | { |
|
759 | return match ($errorCode) { |
||
760 | '#NULL!' => pack('C', 0x00), |
||
761 | '#DIV/0!' => pack('C', 0x07), |
||
762 | '#VALUE!' => pack('C', 0x0F), |
||
763 | 6 | '#REF!' => pack('C', 0x17), |
|
764 | '#NAME?' => pack('C', 0x1D), |
||
765 | 6 | '#NUM!' => pack('C', 0x24), |
|
766 | '#N/A' => pack('C', 0x2A), |
||
767 | default => pack('C', 0xFF), |
||
768 | }; |
||
769 | 6 | } |
|
770 | |||
771 | private bool $tryDefinedName = false; |
||
772 | |||
773 | private function convertDefinedName(string $name): string |
||
774 | { |
||
775 | if (strlen($name) > 255) { |
||
776 | throw new WriterException('Defined Name is too long'); |
||
777 | } |
||
778 | |||
779 | if ($this->tryDefinedName) { |
||
780 | // @codeCoverageIgnoreStart |
||
781 | $nameReference = 1; |
||
782 | foreach ($this->spreadsheet->getDefinedNames() as $definedName) { |
||
783 | if ($name === $definedName->getName()) { |
||
784 | break; |
||
785 | 6 | } |
|
786 | ++$nameReference; |
||
787 | } |
||
788 | |||
789 | $ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference); |
||
790 | |||
791 | return $ptgRef; |
||
792 | // @codeCoverageIgnoreEnd |
||
793 | } |
||
794 | |||
795 | throw new WriterException('Cannot yet write formulae with defined names to Xls'); |
||
796 | } |
||
797 | 9 | ||
798 | /** |
||
799 | 9 | * Look up the REF index that corresponds to an external sheet name |
|
800 | 9 | * (or range). If it doesn't exist yet add it to the workbook's references |
|
801 | * array. It assumes all sheet names given must exist. |
||
802 | * |
||
803 | 9 | * @param string $ext_ref The name of the external reference |
|
804 | * |
||
805 | * @return string The reference index in packed() format on success |
||
806 | */ |
||
807 | private function getRefIndex(string $ext_ref): string |
||
808 | { |
||
809 | $ext_ref = Preg::replace(["/^'/", "/'$/"], ['', ''], $ext_ref); // Remove leading and trailing ' if any. |
||
810 | $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with ' |
||
811 | |||
812 | // Check if there is a sheet range eg., Sheet1:Sheet2. |
||
813 | if (Preg::isMatch('/:/', $ext_ref)) { |
||
814 | [$sheet_name1, $sheet_name2] = explode(':', $ext_ref); |
||
815 | |||
816 | $sheet1 = $this->getSheetIndex($sheet_name1); |
||
817 | if ($sheet1 == -1) { |
||
818 | throw new WriterException("Unknown sheet name $sheet_name1 in formula"); |
||
819 | } |
||
820 | 9 | $sheet2 = $this->getSheetIndex($sheet_name2); |
|
821 | 9 | if ($sheet2 == -1) { |
|
822 | 1 | throw new WriterException("Unknown sheet name $sheet_name2 in formula"); |
|
823 | } |
||
824 | 8 | ||
825 | // Reverse max and min sheet numbers if necessary |
||
826 | if ($sheet1 > $sheet2) { |
||
827 | [$sheet1, $sheet2] = [$sheet2, $sheet1]; |
||
828 | 8 | } |
|
829 | 8 | } else { // Single sheet name only. |
|
830 | 8 | $sheet1 = $this->getSheetIndex($ext_ref); |
|
831 | 8 | if ($sheet1 == -1) { |
|
832 | 8 | throw new WriterException("Unknown sheet name $ext_ref in formula"); |
|
833 | 8 | } |
|
834 | 8 | $sheet2 = $sheet1; |
|
835 | } |
||
836 | 8 | ||
837 | // assume all references belong to this document |
||
838 | $supbook_index = 0x00; |
||
839 | $ref = pack('vvv', $supbook_index, $sheet1, $sheet2); |
||
840 | 8 | $totalreferences = count($this->references); |
|
841 | $index = -1; |
||
842 | for ($i = 0; $i < $totalreferences; ++$i) { |
||
843 | if ($ref == $this->references[$i]) { |
||
844 | $index = $i; |
||
845 | 8 | ||
846 | break; |
||
847 | } |
||
848 | } |
||
849 | // if REF was not found add it to references array |
||
850 | if ($index == -1) { |
||
851 | $this->references[$totalreferences] = $ref; |
||
852 | $index = $totalreferences; |
||
853 | } |
||
854 | |||
855 | return pack('v', $index); |
||
856 | } |
||
857 | 9 | ||
858 | /** |
||
859 | 9 | * Look up the index that corresponds to an external sheet name. The hash of |
|
860 | 1 | * sheet names is updated by the addworksheet() method of the |
|
861 | * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. |
||
862 | * |
||
863 | 8 | * @param string $sheet_name Sheet name |
|
864 | * |
||
865 | * @return int The sheet index, -1 if the sheet was not found |
||
866 | */ |
||
867 | private function getSheetIndex(string $sheet_name): int |
||
868 | { |
||
869 | if (!isset($this->externalSheets[$sheet_name])) { |
||
870 | return -1; |
||
871 | } |
||
872 | |||
873 | return $this->externalSheets[$sheet_name]; |
||
874 | } |
||
875 | |||
876 | 115 | /** |
|
877 | * This method is used to update the array of sheet names. It is |
||
878 | 115 | * called by the addWorksheet() method of the |
|
879 | * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. |
||
880 | * |
||
881 | * @param string $name The name of the worksheet being added |
||
882 | * @param int $index The index of the worksheet being added |
||
883 | * |
||
884 | * @see Workbook::addWorksheet |
||
885 | */ |
||
886 | public function setExtSheet(string $name, int $index): void |
||
887 | { |
||
888 | 41 | $this->externalSheets[$name] = $index; |
|
889 | } |
||
890 | 41 | ||
891 | 41 | /** |
|
892 | 41 | * pack() row and column into the required 3 or 4 byte format. |
|
893 | * |
||
894 | * @param string $cell The Excel cell reference to be packed |
||
895 | 41 | * |
|
896 | * @return array{string, string} Array containing the row and column in packed() format |
||
1 ignored issue
–
show
|
|||
897 | */ |
||
898 | private function cellToPackedRowcol(string $cell): array |
||
899 | { |
||
900 | 41 | $cell = strtoupper($cell); |
|
901 | 41 | [$row, $col, $row_rel, $col_rel] = $this->cellToRowcol($cell); |
|
902 | 41 | if ($col >= 256) { |
|
903 | throw new WriterException("Column in: $cell greater than 255"); |
||
904 | 41 | } |
|
905 | if ($row >= 65536) { |
||
906 | 41 | throw new WriterException("Row in: $cell greater than 65536 "); |
|
907 | } |
||
908 | |||
909 | // Set the high bits to indicate if row or col are relative. |
||
910 | $col |= $col_rel << 14; |
||
911 | $col |= $row_rel << 15; |
||
912 | $col = pack('v', $col); |
||
913 | |||
914 | $row = pack('v', $row); |
||
915 | |||
916 | return [$row, $col]; |
||
917 | } |
||
918 | |||
919 | /** |
||
920 | * pack() row range into the required 3 or 4 byte format. |
||
921 | * Just using maximum col/rows, which is probably not the correct solution. |
||
922 | * |
||
923 | * @param string $range The Excel range to be packed |
||
924 | * |
||
925 | * @return array{string, string, string, string} Array containing (row1,col1,row2,col2) in packed() format |
||
1 ignored issue
–
show
|
|||
926 | */ |
||
927 | private function rangeToPackedRange(string $range): array |
||
928 | { |
||
929 | if (!Preg::isMatch('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match)) { |
||
930 | // @codeCoverageIgnoreStart |
||
931 | throw new WriterException('Regexp failure in rangeToPackedRange'); |
||
932 | // @codeCoverageIgnoreEnd |
||
933 | } |
||
934 | // return absolute rows if there is a $ in the ref |
||
935 | $row1_rel = empty($match[1]) ? 1 : 0; |
||
936 | $row1 = $match[2]; |
||
937 | $row2_rel = empty($match[3]) ? 1 : 0; |
||
938 | $row2 = $match[4]; |
||
939 | // Convert 1-index to zero-index |
||
940 | --$row1; |
||
941 | --$row2; |
||
942 | // Trick poor inocent Excel |
||
943 | $col1 = 0; |
||
944 | $col2 = 65535; // FIXME: maximum possible value for Excel 5 (change this!!!) |
||
945 | |||
946 | // FIXME: this changes for BIFF8 |
||
947 | if (($row1 >= 65536) || ($row2 >= 65536)) { |
||
948 | throw new WriterException("Row in: $range greater than 65536 "); |
||
949 | } |
||
950 | |||
951 | // Set the high bits to indicate if rows are relative. |
||
952 | $col1 |= $row1_rel << 15; |
||
953 | $col2 |= $row2_rel << 15; |
||
954 | $col1 = pack('v', $col1); |
||
955 | $col2 = pack('v', $col2); |
||
956 | |||
957 | $row1 = pack('v', $row1); |
||
958 | $row2 = pack('v', $row2); |
||
959 | |||
960 | 41 | return [$row1, $col1, $row2, $col2]; |
|
961 | } |
||
962 | 41 | ||
963 | /** |
||
964 | * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero |
||
965 | * indexed row and column number. Also returns two (0,1) values to indicate |
||
966 | * whether the row or column are relative references. |
||
967 | * |
||
968 | 41 | * @param string $cell the Excel cell reference in A1 format |
|
969 | 41 | * |
|
970 | 41 | * @return array{int, int, int, int} |
|
1 ignored issue
–
show
|
|||
971 | 41 | */ |
|
972 | private function cellToRowcol(string $cell): array |
||
973 | { |
||
974 | 41 | if (!Preg::isMatch('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match)) { |
|
975 | 41 | // @codeCoverageIgnoreStart |
|
976 | 41 | throw new WriterException('Regexp failure in cellToRowcol'); |
|
977 | 41 | // @codeCoverageIgnoreEnd |
|
978 | 41 | } |
|
979 | 41 | // return absolute column if there is a $ in the ref |
|
980 | $col_rel = empty($match[1]) ? 1 : 0; |
||
981 | $col_ref = $match[2]; |
||
982 | $row_rel = empty($match[3]) ? 1 : 0; |
||
983 | 41 | $row = $match[4]; |
|
984 | 41 | ||
985 | // Convert base26 column string to a number. |
||
986 | 41 | $expn = strlen($col_ref) - 1; |
|
987 | $col = 0; |
||
988 | $col_ref_length = strlen($col_ref); |
||
989 | for ($i = 0; $i < $col_ref_length; ++$i) { |
||
990 | $col += (ord($col_ref[$i]) - 64) * 26 ** $expn; |
||
991 | --$expn; |
||
992 | 51 | } |
|
993 | |||
994 | 51 | // Convert 1-index to zero-index |
|
995 | 51 | --$row; |
|
996 | 51 | --$col; |
|
997 | 51 | ||
998 | return [(int) $row, (int) $col, $row_rel, $col_rel]; |
||
999 | 51 | } |
|
1000 | 51 | ||
1001 | 4 | /** |
|
1002 | * Advance to the next valid token. |
||
1003 | */ |
||
1004 | 51 | private function advance(): void |
|
1005 | 50 | { |
|
1006 | $token = ''; |
||
1007 | 51 | $i = $this->currentCharacter; |
|
1008 | $formula = mb_str_split($this->formula, 1, self::UTF8); |
||
1009 | $formula_length = count($formula); |
||
1010 | 51 | // eat up white spaces |
|
1011 | 51 | if ($i < $formula_length) { |
|
1012 | while ($formula[$i] === ' ') { |
||
1013 | 51 | ++$i; |
|
1014 | 50 | } |
|
1015 | |||
1016 | 51 | if ($i < ($formula_length - 1)) { |
|
1017 | $this->lookAhead = $formula[$i + 1]; |
||
1018 | } |
||
1019 | 51 | $token = ''; |
|
1020 | 51 | } |
|
1021 | 51 | ||
1022 | while ($i < $formula_length) { |
||
1023 | 51 | $token .= $formula[$i]; |
|
1024 | |||
1025 | if ($i < ($formula_length - 1)) { |
||
1026 | 50 | $this->lookAhead = $formula[$i + 1]; |
|
1027 | 47 | } else { |
|
1028 | $this->lookAhead = ''; |
||
1029 | 35 | } |
|
1030 | |||
1031 | 50 | if ($this->match($token) !== '') { |
|
1032 | $this->currentCharacter = $i + 1; |
||
1033 | $this->currentToken = $token; |
||
1034 | |||
1035 | return; |
||
1036 | } |
||
1037 | |||
1038 | if ($i < ($formula_length - 2)) { |
||
1039 | $this->lookAhead = $formula[$i + 2]; |
||
1040 | } else { // if we run out of characters lookAhead becomes empty |
||
1041 | $this->lookAhead = ''; |
||
1042 | 51 | } |
|
1043 | ++$i; |
||
1044 | } |
||
1045 | 51 | } |
|
1046 | 51 | ||
1047 | 51 | /** |
|
1048 | 51 | * Checks if it's a valid token. |
|
1049 | 51 | * |
|
1050 | 51 | * @param string $token the token to check |
|
1051 | 51 | * |
|
1052 | 51 | * @return string The checked token or empty string on failure |
|
1053 | 51 | */ |
|
1054 | 51 | private function match(string $token): string |
|
1055 | 51 | { |
|
1056 | 51 | switch ($token) { |
|
1057 | 51 | case '+': |
|
1058 | 51 | case '-': |
|
1059 | 51 | case '*': |
|
1060 | 45 | case '/': |
|
1061 | case '(': |
||
1062 | 51 | case ')': |
|
1063 | 2 | case ',': |
|
1064 | 1 | case ';': |
|
1065 | case '>=': |
||
1066 | case '<=': |
||
1067 | 2 | case '=': |
|
1068 | case '<>': |
||
1069 | 51 | case '^': |
|
1070 | case '&': |
||
1071 | 8 | case '%': |
|
1072 | 8 | return $token; |
|
1073 | |||
1074 | case '>': |
||
1075 | 1 | if ($this->lookAhead === '=') { // it's a GE token |
|
1076 | break; |
||
1077 | } |
||
1078 | |||
1079 | return $token; |
||
1080 | 51 | ||
1081 | 51 | case '<': |
|
1082 | 51 | // it's a LE or a NE token |
|
1083 | 51 | if (($this->lookAhead === '=') || ($this->lookAhead === '>')) { |
|
1084 | 51 | break; |
|
1085 | } |
||
1086 | 30 | ||
1087 | return $token; |
||
1088 | } |
||
1089 | |||
1090 | 51 | // if it's a reference A1 or $A$1 or $A1 or A$1 |
|
1091 | 51 | if ( |
|
1092 | 51 | Preg::isMatch('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) |
|
1093 | 51 | && !Preg::isMatch('/\d/', $this->lookAhead) |
|
1094 | && ($this->lookAhead !== ':') |
||
1095 | 1 | && ($this->lookAhead !== '.') |
|
1096 | && ($this->lookAhead !== '!') |
||
1097 | ) { |
||
1098 | return $token; |
||
1099 | 51 | } |
|
1100 | 51 | // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) |
|
1101 | 51 | if ( |
|
1102 | Preg::isMatch('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\!\$?[A-Ia-i]?[A-Za-z]\$?\d+$/u', $token) |
||
1103 | 8 | && !Preg::isMatch('/\d/', $this->lookAhead) |
|
1104 | && ($this->lookAhead !== ':') |
||
1105 | && ($this->lookAhead !== '.') |
||
1106 | ) { |
||
1107 | 51 | return $token; |
|
1108 | 51 | } |
|
1109 | 51 | // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) |
|
1110 | 51 | if ( |
|
1111 | 51 | self::matchCellSheetnameQuoted($token) |
|
1112 | && !Preg::isMatch('/\d/', $this->lookAhead) |
||
1113 | 26 | && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') |
|
1114 | ) { |
||
1115 | return $token; |
||
1116 | } |
||
1117 | 51 | // if it's a range A1:A2 or $A$1:$A$2 |
|
1118 | 51 | if ( |
|
1119 | 51 | Preg::isMatch( |
|
1120 | 51 | '/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', |
|
1121 | 51 | $token |
|
1122 | 51 | ) |
|
1123 | 51 | && !Preg::isMatch('/\d/', $this->lookAhead) |
|
1124 | 51 | ) { |
|
1125 | return $token; |
||
1126 | } |
||
1127 | // If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 |
||
1128 | if ( |
||
1129 | Preg::isMatch( |
||
1130 | 51 | '/^' |
|
1131 | 51 | . self::REGEX_SHEET_TITLE_UNQUOTED |
|
1132 | . '(\:' . self::REGEX_SHEET_TITLE_UNQUOTED |
||
1133 | 4 | . ')?\!\$?([A-Ia-i]?[A-Za-z])?\$?\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\d+$/u', |
|
1134 | $token |
||
1135 | ) |
||
1136 | 51 | && !Preg::isMatch('/\d/', $this->lookAhead) |
|
1137 | 30 | ) { |
|
1138 | return $token; |
||
1139 | } |
||
1140 | 50 | // If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 |
|
1141 | 50 | if ( |
|
1142 | 50 | self::matchRangeSheetnameQuoted($token) |
|
1143 | && !Preg::isMatch('/\d/', $this->lookAhead) |
||
1144 | ) { |
||
1145 | 24 | return $token; |
|
1146 | } |
||
1147 | // If it's a number (check that it's not a sheet name or range) |
||
1148 | if (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) { |
||
1149 | 50 | return $token; |
|
1150 | 50 | } |
|
1151 | if ( |
||
1152 | 1 | Preg::isMatch('/"([^"]|""){0,255}"/', $token) |
|
1153 | && $this->lookAhead !== '"' |
||
1154 | && (substr_count($token, '"') % 2 == 0) |
||
1155 | ) { |
||
1156 | 50 | // If it's a string (of maximum 255 characters) |
|
1157 | 50 | return $token; |
|
1158 | } |
||
1159 | 34 | // If it's an error code |
|
1160 | if ( |
||
1161 | Preg::isMatch('/^#[A-Z0\/]{3,5}[!?]{1}$/', $token) |
||
1162 | 50 | || $token === '#N/A' |
|
1163 | 50 | ) { |
|
1164 | 50 | return $token; |
|
1165 | 50 | } |
|
1166 | 50 | // if it's a function call |
|
1167 | 50 | if ( |
|
1168 | 50 | Preg::isMatch("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) |
|
1169 | && ($this->lookAhead === '(') |
||
1170 | 6 | ) { |
|
1171 | return $token; |
||
1172 | } |
||
1173 | 50 | if ( |
|
1174 | 50 | Preg::isMatch( |
|
1175 | '/^' |
||
1176 | 2 | . Calculation::CALCULATION_REGEXP_DEFINEDNAME |
|
1177 | . '$/miu', |
||
1178 | $token |
||
1179 | 50 | ) |
|
1180 | 50 | && $this->spreadsheet->getDefinedName($token) !== null |
|
1181 | ) { |
||
1182 | 2 | return $token; |
|
1183 | } |
||
1184 | 50 | if ( |
|
1185 | Preg::isMatch('/^true$/i', $token) |
||
1186 | && ($this->lookAhead === ')' || $this->lookAhead === ',') |
||
1187 | 1 | ) { |
|
1188 | return $token; |
||
1189 | } |
||
1190 | 50 | if ( |
|
1191 | Preg::isMatch('/^false$/i', $token) |
||
1192 | && ($this->lookAhead === ')' || $this->lookAhead === ',') |
||
1193 | ) { |
||
1194 | return $token; |
||
1195 | } |
||
1196 | if (str_ends_with($token, ')')) { |
||
1197 | // It's an argument of some description (e.g. a named range), |
||
1198 | // precise nature yet to be determined |
||
1199 | return $token; |
||
1200 | } |
||
1201 | 51 | ||
1202 | return ''; |
||
1203 | 51 | } |
|
1204 | 51 | ||
1205 | 51 | /** |
|
1206 | 51 | * The parsing method. It parses a formula. |
|
1207 | 51 | * |
|
1208 | * @param string $formula the formula to parse, without the initial equal |
||
1209 | 51 | * sign (=) |
|
1210 | * |
||
1211 | * @return bool true on success |
||
1212 | */ |
||
1213 | public function parse(string $formula): bool |
||
1214 | { |
||
1215 | $this->currentCharacter = 0; |
||
1216 | $this->formula = $formula; |
||
1217 | $this->lookAhead = mb_substr($formula, 1, 1, self::UTF8); |
||
1218 | 51 | $this->advance(); |
|
1219 | $this->parseTree = $this->condition(); |
||
1220 | 51 | ||
1221 | 51 | return true; |
|
1222 | 1 | } |
|
1223 | 1 | ||
1224 | 1 | /** |
|
1225 | 51 | * It parses a condition. It assumes the following rule: |
|
1226 | 2 | * Cond -> Expr [(">" | "<") Expr]. |
|
1227 | 2 | * |
|
1228 | 2 | * @return mixed[] The parsed ptg'd tree on success |
|
1229 | 51 | */ |
|
1230 | 2 | private function condition(): array |
|
1231 | 2 | { |
|
1232 | 2 | $result = $this->expression(); |
|
1233 | 51 | if ($this->currentToken == '<') { |
|
1234 | 1 | $this->advance(); |
|
1235 | 1 | $result2 = $this->expression(); |
|
1236 | 1 | $result = $this->createTree('ptgLT', $result, $result2); |
|
1237 | 51 | } elseif ($this->currentToken == '>') { |
|
1238 | 4 | $this->advance(); |
|
1239 | 4 | $result2 = $this->expression(); |
|
1240 | 4 | $result = $this->createTree('ptgGT', $result, $result2); |
|
1241 | 51 | } elseif ($this->currentToken == '<=') { |
|
1242 | 7 | $this->advance(); |
|
1243 | 7 | $result2 = $this->expression(); |
|
1244 | 7 | $result = $this->createTree('ptgLE', $result, $result2); |
|
1245 | } elseif ($this->currentToken == '>=') { |
||
1246 | $this->advance(); |
||
1247 | 51 | $result2 = $this->expression(); |
|
1248 | $result = $this->createTree('ptgGE', $result, $result2); |
||
1249 | } elseif ($this->currentToken == '=') { |
||
1250 | $this->advance(); |
||
1251 | $result2 = $this->expression(); |
||
1252 | $result = $this->createTree('ptgEQ', $result, $result2); |
||
1253 | } elseif ($this->currentToken == '<>') { |
||
1254 | $this->advance(); |
||
1255 | $result2 = $this->expression(); |
||
1256 | $result = $this->createTree('ptgNE', $result, $result2); |
||
1257 | } |
||
1258 | |||
1259 | return $result; |
||
1260 | 51 | } |
|
1261 | |||
1262 | /** |
||
1263 | 51 | * It parses a expression. It assumes the following rule: |
|
1264 | 24 | * Expr -> Term [("+" | "-") Term] |
|
1265 | 24 | * -> "string" |
|
1266 | * -> "-" Term : Negative value |
||
1267 | 7 | * -> "+" Term : Positive value |
|
1268 | * -> Error code. |
||
1269 | 24 | * |
|
1270 | 24 | * @return mixed[] The parsed ptg'd tree on success |
|
1271 | */ |
||
1272 | 24 | private function expression(): array |
|
1273 | { |
||
1274 | // If it's a string return a string node |
||
1275 | 51 | if (Preg::isMatch('/"([^"]|""){0,255}"/', $this->currentToken)) { |
|
1276 | 51 | $tmp = str_replace('""', '"', $this->currentToken); |
|
1277 | if (($tmp == '"') || ($tmp == '')) { |
||
1278 | 1 | // Trap for "" that has been used for an empty string |
|
1279 | 1 | $tmp = '""'; |
|
1280 | } |
||
1281 | 1 | $result = $this->createTree($tmp, '', ''); |
|
1282 | $this->advance(); |
||
1283 | 51 | ||
1284 | return $result; |
||
1285 | 6 | } |
|
1286 | 6 | if ( |
|
1287 | Preg::isMatch('/^#[A-Z0\/]{3,5}[!?]{1}$/', $this->currentToken) |
||
1288 | 6 | || $this->currentToken == '#N/A' |
|
1289 | 51 | ) { // error code |
|
1290 | $result = $this->createTree($this->currentToken, 'ptgErr', ''); |
||
1291 | 1 | $this->advance(); |
|
1292 | 1 | ||
1293 | return $result; |
||
1294 | 1 | } |
|
1295 | if ($this->currentToken == '-') { // negative value |
||
1296 | 51 | // catch "-" Term |
|
1297 | 50 | $this->advance(); |
|
1298 | 8 | $result2 = $this->expression(); |
|
1299 | 8 | ||
1300 | 8 | return $this->createTree('ptgUminus', $result2, ''); |
|
1301 | } elseif ($this->currentToken == '+') { // positive value |
||
1302 | // catch "+" Term |
||
1303 | 50 | $this->advance(); |
|
1304 | 50 | $result2 = $this->expression(); |
|
1305 | 50 | ||
1306 | return $this->createTree('ptgUplus', $result2, ''); |
||
1307 | 21 | } |
|
1308 | 21 | $result = $this->term(); |
|
1309 | 21 | while ($this->currentToken === '&') { |
|
1310 | 21 | $this->advance(); |
|
1311 | 6 | $result2 = $this->expression(); |
|
1312 | 6 | $result = $this->createTree('ptgConcat', $result, $result2); |
|
1313 | 6 | } |
|
1314 | 6 | while ( |
|
1315 | ($this->currentToken == '+') |
||
1316 | 1 | || ($this->currentToken == '-') |
|
1317 | 1 | || ($this->currentToken == '^') |
|
1318 | 1 | ) { |
|
1319 | if ($this->currentToken == '+') { |
||
1320 | $this->advance(); |
||
1321 | $result2 = $this->term(); |
||
1322 | 50 | $result = $this->createTree('ptgAdd', $result, $result2); |
|
1323 | } elseif ($this->currentToken == '-') { |
||
1324 | $this->advance(); |
||
1325 | $result2 = $this->term(); |
||
1326 | $result = $this->createTree('ptgSub', $result, $result2); |
||
1327 | } else { |
||
1328 | $this->advance(); |
||
1329 | $result2 = $this->term(); |
||
1330 | $result = $this->createTree('ptgPower', $result, $result2); |
||
1331 | } |
||
1332 | } |
||
1333 | 3 | ||
1334 | return $result; |
||
1335 | 3 | } |
|
1336 | |||
1337 | /** |
||
1338 | * This function just introduces a ptgParen element in the tree, so that Excel |
||
1339 | * doesn't get confused when working with a parenthesized formula afterwards. |
||
1340 | * |
||
1341 | * @return mixed[] The parsed ptg'd tree |
||
1342 | * |
||
1343 | * @see fact() |
||
1344 | 51 | */ |
|
1345 | private function parenthesizedExpression(): array |
||
1348 | 50 | } |
|
1349 | 50 | ||
1350 | /** |
||
1351 | 17 | * It parses a term. It assumes the following rule: |
|
1352 | 16 | * Term -> Fact [("*" | "/") Fact]. |
|
1353 | 16 | * |
|
1354 | 16 | * @return mixed[] The parsed ptg'd tree on success |
|
1355 | */ |
||
1356 | 4 | private function term(): array |
|
1357 | 4 | { |
|
1358 | 4 | $result = $this->fact(); |
|
1359 | while ( |
||
1360 | ($this->currentToken == '*') |
||
1361 | || ($this->currentToken == '/') |
||
1362 | 50 | ) { |
|
1363 | if ($this->currentToken == '*') { |
||
1364 | $this->advance(); |
||
1365 | $result2 = $this->fact(); |
||
1366 | $result = $this->createTree('ptgMul', $result, $result2); |
||
1367 | } else { |
||
1368 | $this->advance(); |
||
1369 | $result2 = $this->fact(); |
||
1370 | $result = $this->createTree('ptgDiv', $result, $result2); |
||
1371 | } |
||
1372 | } |
||
1373 | |||
1374 | return $result; |
||
1375 | 51 | } |
|
1376 | |||
1377 | 51 | /** |
|
1378 | 51 | * It parses a factor. It assumes the following rule: |
|
1379 | 3 | * Fact -> ( Expr ) |
|
1380 | 3 | * | CellRef |
|
1381 | 3 | * | CellRange |
|
1382 | * | Number |
||
1383 | * | Function. |
||
1384 | 3 | * |
|
1385 | * @return mixed[] The parsed ptg'd tree on success |
||
1386 | 3 | */ |
|
1387 | private function fact(): array |
||
1388 | { |
||
1389 | 51 | $currentToken = $this->currentToken; |
|
1390 | 30 | if ($currentToken === '(') { |
|
1391 | 30 | $this->advance(); // eat the "(" |
|
1392 | $result = $this->parenthesizedExpression(); |
||
1393 | 30 | if ($this->currentToken !== ')') { |
|
1394 | throw new WriterException("')' token expected."); |
||
1395 | } |
||
1396 | 48 | $this->advance(); // eat the ")" |
|
1397 | 48 | ||
1398 | 48 | return $result; |
|
1399 | 48 | } |
|
1400 | 48 | // if it's a reference |
|
1401 | 48 | if (Preg::isMatch('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) { |
|
1402 | 48 | $result = $this->createTree($this->currentToken, '', ''); |
|
1403 | $this->advance(); |
||
1404 | |||
1405 | 1 | return $result; |
|
1406 | 1 | } |
|
1407 | if ( |
||
1408 | 1 | Preg::isMatch( |
|
1409 | '/^' |
||
1410 | 48 | . self::REGEX_SHEET_TITLE_UNQUOTED |
|
1411 | . '(\:' . self::REGEX_SHEET_TITLE_UNQUOTED |
||
1412 | 8 | . ')?\!\$?[A-Ia-i]?[A-Za-z]\$?\d+$/u', |
|
1413 | 8 | $this->currentToken |
|
1414 | ) |
||
1415 | 8 | ) { |
|
1416 | // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) |
||
1417 | $result = $this->createTree($this->currentToken, '', ''); |
||
1418 | 46 | $this->advance(); |
|
1419 | 46 | ||
1420 | 46 | return $result; |
|
1421 | 46 | } |
|
1422 | 46 | if (self::matchCellSheetnameQuoted($this->currentToken)) { |
|
1423 | 46 | // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) |
|
1424 | 46 | $result = $this->createTree($this->currentToken, '', ''); |
|
1425 | 46 | $this->advance(); |
|
1426 | |||
1427 | return $result; |
||
1428 | } |
||
1429 | 26 | if ( |
|
1430 | 26 | Preg::isMatch( |
|
1431 | '/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', |
||
1432 | 26 | $this->currentToken |
|
1433 | ) |
||
1434 | || Preg::isMatch( |
||
1435 | 46 | '/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', |
|
1436 | 46 | $this->currentToken |
|
1437 | 46 | ) |
|
1438 | 46 | ) { |
|
1439 | 46 | // if it's a range A1:B2 or $A$1:$B$2 |
|
1440 | 46 | // must be an error? |
|
1441 | 46 | $result = $this->createTree($this->currentToken, '', ''); |
|
1442 | 46 | $this->advance(); |
|
1443 | |||
1444 | return $result; |
||
1445 | } |
||
1446 | if ( |
||
1447 | Preg::isMatch( |
||
1448 | '/^' |
||
1449 | . self::REGEX_SHEET_TITLE_UNQUOTED |
||
1450 | . '(\:' |
||
1451 | 46 | . self::REGEX_SHEET_TITLE_UNQUOTED |
|
1452 | . ')?\!\$?([A-Ia-i]?[A-Za-z])?\$?\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\d+$/u', |
||
1453 | $this->currentToken |
||
1454 | 4 | ) |
|
1455 | 4 | ) { |
|
1456 | // If it's an external range (Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2) |
||
1457 | 4 | // must be an error? |
|
1458 | $result = $this->createTree($this->currentToken, '', ''); |
||
1459 | 46 | $this->advance(); |
|
1460 | |||
1461 | 30 | return $result; |
|
1462 | 1 | } |
|
1463 | 1 | if (self::matchRangeSheetnameQuoted($this->currentToken)) { |
|
1464 | // If it's an external range ('Sheet1'!A1:B2 or 'Sheet1'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1'!$A$1:$B$2) |
||
1465 | 30 | // must be an error? |
|
1466 | $result = $this->createTree($this->currentToken, '', ''); |
||
1467 | 30 | $this->advance(); |
|
1468 | |||
1469 | 30 | return $result; |
|
1470 | } |
||
1471 | if (is_numeric($this->currentToken)) { |
||
1472 | 39 | // If it's a number or a percent |
|
1473 | 39 | if ($this->lookAhead === '%') { |
|
1474 | $result = $this->createTree('ptgPercent', $this->currentToken, ''); |
||
1475 | $this->advance(); // Skip the percentage operator once we've pre-built that tree |
||
1476 | 34 | } else { |
|
1477 | $result = $this->createTree($this->currentToken, '', ''); |
||
1478 | } |
||
1479 | 12 | $this->advance(); |
|
1480 | 12 | ||
1481 | 12 | return $result; |
|
1482 | 12 | } |
|
1483 | 12 | if ( |
|
1484 | 12 | Preg::isMatch("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) |
|
1485 | 12 | && ($this->lookAhead === '(') |
|
1486 | ) { |
||
1487 | 6 | // if it's a function call |
|
1488 | 6 | return $this->func(); |
|
1489 | } |
||
1490 | 6 | if ( |
|
1491 | Preg::isMatch( |
||
1492 | 6 | '/^' |
|
1493 | 2 | . Calculation::CALCULATION_REGEXP_DEFINEDNAME |
|
1494 | 2 | . '$/miu', |
|
1495 | $this->currentToken |
||
1496 | 2 | ) |
|
1497 | && $this->spreadsheet->getDefinedName($this->currentToken) !== null |
||
1498 | ) { |
||
1499 | 4 | $result = $this->createTree('ptgName', $this->currentToken, ''); |
|
1500 | $this->advance(); |
||
1501 | |||
1502 | return $result; |
||
1503 | } |
||
1504 | if (Preg::isMatch('/^true|false$/i', $this->currentToken)) { |
||
1505 | $result = $this->createTree($this->currentToken, '', ''); |
||
1506 | $this->advance(); |
||
1507 | |||
1508 | 34 | return $result; |
|
1509 | } |
||
1510 | 34 | ||
1511 | 34 | throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter); |
|
1512 | 34 | } |
|
1513 | 34 | ||
1514 | 34 | /** |
|
1515 | 34 | * It parses a function call. It assumes the following rule: |
|
1516 | 34 | * Func -> ( Expr [,Expr]* ). |
|
1517 | 20 | * |
|
1518 | 20 | * @return mixed[] The parsed ptg'd tree on success |
|
1519 | */ |
||
1520 | private function func(): array |
||
1521 | { |
||
1522 | 20 | $num_args = 0; // number of arguments received |
|
1523 | 20 | $function = strtoupper($this->currentToken); |
|
1524 | $result = ''; // initialize result |
||
1525 | 34 | $this->advance(); |
|
1526 | 34 | $this->advance(); // eat the "(" |
|
1527 | while ($this->currentToken !== ')') { |
||
1528 | 34 | if ($num_args > 0) { |
|
1529 | if ($this->currentToken === ',' || $this->currentToken === ';') { |
||
1530 | 34 | $this->advance(); // eat the "," or ";" |
|
1531 | 3 | } else { |
|
1532 | throw new WriterException("Syntax error: comma expected in function $function, arg #{$num_args}"); |
||
1533 | 34 | } |
|
1534 | $result2 = $this->condition(); |
||
1535 | 34 | $result = $this->createTree('arg', $result, $result2); |
|
1536 | } else { // first argument |
||
1537 | $result2 = $this->condition(); |
||
1538 | $result = $this->createTree('arg', '', $result2); |
||
1539 | 34 | } |
|
1540 | 34 | ++$num_args; |
|
1541 | } |
||
1542 | 34 | if (!isset($this->functions[$function])) { |
|
1543 | throw new WriterException("Function $function() doesn't exist"); |
||
1544 | } |
||
1545 | $args = $this->functions[$function][1]; |
||
1546 | // If fixed number of args eg. TIME($i, $j, $k). Check that the number of args is valid. |
||
1547 | if (($args >= 0) && ($args != $num_args)) { |
||
1548 | throw new WriterException("Incorrect number of arguments in function $function() "); |
||
1549 | } |
||
1550 | |||
1551 | $result = $this->createTree($function, $result, $num_args); |
||
1552 | $this->advance(); // eat the ")" |
||
1553 | |||
1554 | return $result; |
||
1555 | 51 | } |
|
1556 | |||
1557 | 51 | /** |
|
1558 | * Creates a tree. In fact an array which may have one or two arrays (sub-trees) |
||
1559 | * as elements. |
||
1560 | * |
||
1561 | * @param mixed $value the value of this node |
||
1562 | * @param mixed $left the left array (sub-tree) or a final node |
||
1563 | * @param mixed $right the right array (sub-tree) or a final node |
||
1564 | * |
||
1565 | * @return mixed[] A tree |
||
1566 | */ |
||
1567 | private function createTree(mixed $value, mixed $left, mixed $right): array |
||
1568 | { |
||
1569 | return ['value' => $value, 'left' => $left, 'right' => $right]; |
||
1570 | } |
||
1571 | |||
1572 | /** |
||
1573 | * Builds a string containing the tree in reverse polish notation (What you |
||
1574 | * would use in a HP calculator stack). |
||
1575 | * The following tree:. |
||
1576 | * |
||
1577 | * + |
||
1578 | * / \ |
||
1579 | * 2 3 |
||
1580 | * |
||
1581 | * produces: "23+" |
||
1582 | * |
||
1583 | * The following tree: |
||
1584 | * |
||
1585 | * + |
||
1586 | * / \ |
||
1587 | 55 | * 3 * |
|
1588 | * / \ |
||
1589 | 55 | * 6 A1 |
|
1590 | 55 | * |
|
1591 | 52 | * produces: "36A1*+" |
|
1592 | * |
||
1593 | 55 | * In fact all operands, functions, references, etc... are written as ptg's |
|
1594 | 2 | * |
|
1595 | * @param mixed[] $tree the optional tree to convert |
||
1596 | * |
||
1597 | 53 | * @return string The tree in reverse polish notation |
|
1598 | 45 | */ |
|
1599 | 45 | public function toReversePolish(array $tree = []): string |
|
1600 | 53 | { |
|
1601 | 9 | $polish = ''; // the string we are going to return |
|
1602 | 3 | if (empty($tree)) { // If it's the first call use parseTree |
|
1603 | $tree = $this->parseTree; |
||
1604 | 53 | } |
|
1605 | 44 | if (!is_array($tree) || !isset($tree['left'], $tree['right'], $tree['value'])) { |
|
1606 | 43 | throw new WriterException('Unexpected non-array'); |
|
1607 | 53 | } |
|
1608 | 35 | ||
1609 | 35 | if (is_array($tree['left'])) { |
|
1610 | $converted_tree = $this->toReversePolish($tree['left']); |
||
1611 | $polish .= $converted_tree; |
||
1612 | } elseif ($tree['left'] != '') { // It's a final node |
||
1613 | 53 | $converted_tree = $this->convert($tree['left']); //* @phpstan-ignore-line |
|
1614 | 53 | $polish .= $converted_tree; |
|
1615 | 53 | } |
|
1616 | 53 | if (is_array($tree['right'])) { |
|
1617 | 53 | $converted_tree = $this->toReversePolish($tree['right']); |
|
1618 | 53 | $polish .= $converted_tree; |
|
1619 | 53 | } elseif ($tree['right'] != '') { // It's a final node |
|
1620 | 53 | $converted_tree = $this->convert(StringHelper::convertToString($tree['right'])); |
|
1621 | $polish .= $converted_tree; |
||
1622 | } |
||
1623 | 33 | // if it's a function convert it here (so we can set it's arguments) |
|
1624 | 33 | /** @var string */ |
|
1625 | $treeValueString = $tree['value']; |
||
1626 | 9 | if ( |
|
1627 | Preg::isMatch("/^[A-Z0-9\xc0-\xdc\\.]+$/", $treeValueString) |
||
1628 | && !Preg::isMatch('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $treeValueString) |
||
1629 | && !Preg::isMatch( |
||
1630 | 33 | '/^[A-Ia-i]?[A-Za-z](\d+)\.\.[A-Ia-i]?[A-Za-z](\d+)$/', |
|
1631 | 33 | $treeValueString |
|
1632 | ) |
||
1633 | && !is_numeric($treeValueString) |
||
1634 | 53 | && !isset($this->ptg[$treeValueString]) |
|
1635 | ) { |
||
1636 | 51 | // left subtree for a function is always an array. |
|
1637 | if ($tree['left'] != '') { |
||
1638 | $left_tree = $this->toReversePolish($tree['left']); //* @phpstan-ignore-line |
||
1639 | 62 | } else { |
|
1640 | $left_tree = ''; |
||
1641 | 62 | } |
|
1642 | 62 | ||
1643 | 62 | // add its left subtree and return. |
|
1644 | 62 | if ($left_tree !== '' || $tree['right'] !== '') { |
|
1645 | /** @var string */ |
||
1646 | $treeValueString = $tree['value']; |
||
1647 | 62 | /** @var int */ |
|
1648 | $treeRightInt = is_numeric($tree['right']) ? ((int) $tree['right']) : 0; |
||
1649 | 62 | ||
1650 | 62 | return $left_tree . $this->convertFunction($treeValueString, $treeRightInt); |
|
1651 | 62 | } |
|
1652 | 62 | } |
|
1653 | /** @var string */ |
||
1654 | $treeValueString = $tree['value']; |
||
1655 | $converted_tree = $this->convert($treeValueString); |
||
1656 | |||
1657 | return $polish . $converted_tree; |
||
1658 | } |
||
1659 | |||
1660 | public static function matchCellSheetnameQuoted(string $token): bool |
||
1665 | ); |
||
1666 | } |
||
1667 | |||
1668 | public static function matchRangeSheetnameQuoted(string $token): bool |
||
1669 | { |
||
1670 | return Preg::isMatch( |
||
1671 | self::REGEX_RANGE_TITLE_QUOTED, |
||
1672 | $token |
||
1673 | ); |
||
1674 | } |
||
1675 | } |
||
1676 |