PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 3.5.1
Visualizer – Tables & Charts Manager with Built-in AI Generator v3.5.1
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 3.10.3 All 149 releases
visualizer / vendor / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Writer / Xls / Parser.php

Parser.php in Visualizer – Tables & Charts Manager with Built-in AI Generator 3.5.1, at vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php

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