Failed Conditions
Push — master ( 42761f...d02352 )
by Adrien
16:26 queued 08:32
created

Parser::getRefIndex()   B

Complexity

Conditions 9
Paths 21

Size

Total Lines 50
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 14.184

Importance

Changes 0
Metric Value
cc 9
eloc 30
nc 21
nop 1
dl 0
loc 50
ccs 18
cts 30
cp 0.6
crap 14.184
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
7
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as PhpspreadsheetWorksheet;
9
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
10
11
// Original file header of PEAR::Spreadsheet_Excel_Writer_Parser (used as the base for this class):
12
// -----------------------------------------------------------------------------------------
13
// *  Class for parsing Excel formulas
14
// *
15
// *  License Information:
16
// *
17
// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
18
// *    Copyright (c) 2002-2003 Xavier Noguer [email protected]
19
// *
20
// *    This library is free software; you can redistribute it and/or
21
// *    modify it under the terms of the GNU Lesser General Public
22
// *    License as published by the Free Software Foundation; either
23
// *    version 2.1 of the License, or (at your option) any later version.
24
// *
25
// *    This library is distributed in the hope that it will be useful,
26
// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
27
// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
28
// *    Lesser General Public License for more details.
29
// *
30
// *    You should have received a copy of the GNU Lesser General Public
31
// *    License along with this library; if not, write to the Free Software
32
// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
33
// */
34
class Parser
35
{
36
    /**    Constants                */
37
    // Sheet title in unquoted form
38
    // Invalid sheet title characters cannot occur in the sheet title:
39
    //         *:/\?[]
40
    // Moreover, there are valid sheet title characters that cannot occur in unquoted form (there may be more?)
41
    // +-% '^&<>=,;#()"{}
42
    const REGEX_SHEET_TITLE_UNQUOTED = '[^\*\:\/\\\\\?\[\]\+\-\% \\\'\^\&\<\>\=\,\;\#\(\)\"\{\}]+';
43
44
    // Sheet title in quoted form (without surrounding quotes)
45
    // Invalid sheet title characters cannot occur in the sheet title:
46
    // *:/\?[]                    (usual invalid sheet title characters)
47
    // Single quote is represented as a pair ''
48
    const REGEX_SHEET_TITLE_QUOTED = '(([^\*\:\/\\\\\?\[\]\\\'])+|(\\\'\\\')+)+';
49
50
    /**
51
     * The index of the character we are currently looking at.
52
     *
53
     * @var int
54
     */
55
    public $currentCharacter;
56
57
    /**
58
     * The token we are working on.
59
     *
60
     * @var string
61
     */
62
    public $currentToken;
63
64
    /**
65
     * The formula to parse.
66
     *
67
     * @var string
68
     */
69
    private $formula;
70
71
    /**
72
     * The character ahead of the current char.
73
     *
74
     * @var string
75
     */
76
    public $lookAhead;
77
78
    /**
79
     * The parse tree to be generated.
80
     *
81
     * @var string
82
     */
83
    public $parseTree;
84
85
    /**
86
     * Array of external sheets.
87
     *
88
     * @var array
89
     */
90
    private $externalSheets;
91
92
    /**
93
     * Array of sheet references in the form of REF structures.
94
     *
95
     * @var array
96
     */
97
    public $references;
98
99
    /**
100
     * The Excel ptg indices.
101
     *
102
     * @var array
103
     */
104
    private $ptg = [
105
        'ptgExp' => 0x01,
106
        'ptgTbl' => 0x02,
107
        'ptgAdd' => 0x03,
108
        'ptgSub' => 0x04,
109
        'ptgMul' => 0x05,
110
        'ptgDiv' => 0x06,
111
        'ptgPower' => 0x07,
112
        'ptgConcat' => 0x08,
113
        'ptgLT' => 0x09,
114
        'ptgLE' => 0x0A,
115
        'ptgEQ' => 0x0B,
116
        'ptgGE' => 0x0C,
117
        'ptgGT' => 0x0D,
118
        'ptgNE' => 0x0E,
119
        'ptgIsect' => 0x0F,
120
        'ptgUnion' => 0x10,
121
        'ptgRange' => 0x11,
122
        'ptgUplus' => 0x12,
123
        'ptgUminus' => 0x13,
124
        'ptgPercent' => 0x14,
125
        'ptgParen' => 0x15,
126
        'ptgMissArg' => 0x16,
127
        'ptgStr' => 0x17,
128
        'ptgAttr' => 0x19,
129
        'ptgSheet' => 0x1A,
130
        'ptgEndSheet' => 0x1B,
131
        'ptgErr' => 0x1C,
132
        'ptgBool' => 0x1D,
133
        'ptgInt' => 0x1E,
134
        'ptgNum' => 0x1F,
135
        'ptgArray' => 0x20,
136
        'ptgFunc' => 0x21,
137
        'ptgFuncVar' => 0x22,
138
        'ptgName' => 0x23,
139
        'ptgRef' => 0x24,
140
        'ptgArea' => 0x25,
141
        'ptgMemArea' => 0x26,
142
        'ptgMemErr' => 0x27,
143
        'ptgMemNoMem' => 0x28,
144
        'ptgMemFunc' => 0x29,
145
        'ptgRefErr' => 0x2A,
146
        'ptgAreaErr' => 0x2B,
147
        'ptgRefN' => 0x2C,
148
        'ptgAreaN' => 0x2D,
149
        'ptgMemAreaN' => 0x2E,
150
        'ptgMemNoMemN' => 0x2F,
151
        'ptgNameX' => 0x39,
152
        'ptgRef3d' => 0x3A,
153
        'ptgArea3d' => 0x3B,
154
        'ptgRefErr3d' => 0x3C,
155
        'ptgAreaErr3d' => 0x3D,
156
        'ptgArrayV' => 0x40,
157
        'ptgFuncV' => 0x41,
158
        'ptgFuncVarV' => 0x42,
159
        'ptgNameV' => 0x43,
160
        'ptgRefV' => 0x44,
161
        'ptgAreaV' => 0x45,
162
        'ptgMemAreaV' => 0x46,
163
        'ptgMemErrV' => 0x47,
164
        'ptgMemNoMemV' => 0x48,
165
        'ptgMemFuncV' => 0x49,
166
        'ptgRefErrV' => 0x4A,
167
        'ptgAreaErrV' => 0x4B,
168
        'ptgRefNV' => 0x4C,
169
        'ptgAreaNV' => 0x4D,
170
        'ptgMemAreaNV' => 0x4E,
171
        'ptgMemNoMemNV' => 0x4F,
172
        'ptgFuncCEV' => 0x58,
173
        'ptgNameXV' => 0x59,
174
        'ptgRef3dV' => 0x5A,
175
        'ptgArea3dV' => 0x5B,
176
        'ptgRefErr3dV' => 0x5C,
177
        'ptgAreaErr3dV' => 0x5D,
178
        'ptgArrayA' => 0x60,
179
        'ptgFuncA' => 0x61,
180
        'ptgFuncVarA' => 0x62,
181
        'ptgNameA' => 0x63,
182
        'ptgRefA' => 0x64,
183
        'ptgAreaA' => 0x65,
184
        'ptgMemAreaA' => 0x66,
185
        'ptgMemErrA' => 0x67,
186
        'ptgMemNoMemA' => 0x68,
187
        'ptgMemFuncA' => 0x69,
188
        'ptgRefErrA' => 0x6A,
189
        'ptgAreaErrA' => 0x6B,
190
        'ptgRefNA' => 0x6C,
191
        'ptgAreaNA' => 0x6D,
192
        'ptgMemAreaNA' => 0x6E,
193
        'ptgMemNoMemNA' => 0x6F,
194
        'ptgFuncCEA' => 0x78,
195
        'ptgNameXA' => 0x79,
196
        'ptgRef3dA' => 0x7A,
197
        'ptgArea3dA' => 0x7B,
198
        'ptgRefErr3dA' => 0x7C,
199
        'ptgAreaErr3dA' => 0x7D,
200
    ];
201
202
    /**
203
     * Thanks to Michael Meeks and Gnumeric for the initial arg values.
204
     *
205
     * The following hash was generated by "function_locale.pl" in the distro.
206
     * Refer to function_locale.pl for non-English function names.
207
     *
208
     * The array elements are as follow:
209
     * ptg:   The Excel function ptg code.
210
     * args:  The number of arguments that the function takes:
211
     *           >=0 is a fixed number of arguments.
212
     *           -1  is a variable  number of arguments.
213
     * class: The reference, value or array class of the function args.
214
     * vol:   The function is volatile.
215
     *
216
     * @var array
217
     */
218
    private $functions = [
219
        // function                  ptg  args  class  vol
220
        'COUNT' => [0, -1, 0, 0],
221
        'IF' => [1, -1, 1, 0],
222
        'ISNA' => [2, 1, 1, 0],
223
        'ISERROR' => [3, 1, 1, 0],
224
        'SUM' => [4, -1, 0, 0],
225
        'AVERAGE' => [5, -1, 0, 0],
226
        'MIN' => [6, -1, 0, 0],
227
        'MAX' => [7, -1, 0, 0],
228
        'ROW' => [8, -1, 0, 0],
229
        'COLUMN' => [9, -1, 0, 0],
230
        'NA' => [10, 0, 0, 0],
231
        'NPV' => [11, -1, 1, 0],
232
        'STDEV' => [12, -1, 0, 0],
233
        'DOLLAR' => [13, -1, 1, 0],
234
        'FIXED' => [14, -1, 1, 0],
235
        'SIN' => [15, 1, 1, 0],
236
        'COS' => [16, 1, 1, 0],
237
        'TAN' => [17, 1, 1, 0],
238
        'ATAN' => [18, 1, 1, 0],
239
        'PI' => [19, 0, 1, 0],
240
        'SQRT' => [20, 1, 1, 0],
241
        'EXP' => [21, 1, 1, 0],
242
        'LN' => [22, 1, 1, 0],
243
        'LOG10' => [23, 1, 1, 0],
244
        'ABS' => [24, 1, 1, 0],
245
        'INT' => [25, 1, 1, 0],
246
        'SIGN' => [26, 1, 1, 0],
247
        'ROUND' => [27, 2, 1, 0],
248
        'LOOKUP' => [28, -1, 0, 0],
249
        'INDEX' => [29, -1, 0, 1],
250
        'REPT' => [30, 2, 1, 0],
251
        'MID' => [31, 3, 1, 0],
252
        'LEN' => [32, 1, 1, 0],
253
        'VALUE' => [33, 1, 1, 0],
254
        'TRUE' => [34, 0, 1, 0],
255
        'FALSE' => [35, 0, 1, 0],
256
        'AND' => [36, -1, 0, 0],
257
        'OR' => [37, -1, 0, 0],
258
        'NOT' => [38, 1, 1, 0],
259
        'MOD' => [39, 2, 1, 0],
260
        'DCOUNT' => [40, 3, 0, 0],
261
        'DSUM' => [41, 3, 0, 0],
262
        'DAVERAGE' => [42, 3, 0, 0],
263
        'DMIN' => [43, 3, 0, 0],
264
        'DMAX' => [44, 3, 0, 0],
265
        'DSTDEV' => [45, 3, 0, 0],
266
        'VAR' => [46, -1, 0, 0],
267
        'DVAR' => [47, 3, 0, 0],
268
        'TEXT' => [48, 2, 1, 0],
269
        'LINEST' => [49, -1, 0, 0],
270
        'TREND' => [50, -1, 0, 0],
271
        'LOGEST' => [51, -1, 0, 0],
272
        'GROWTH' => [52, -1, 0, 0],
273
        'PV' => [56, -1, 1, 0],
274
        'FV' => [57, -1, 1, 0],
275
        'NPER' => [58, -1, 1, 0],
276
        'PMT' => [59, -1, 1, 0],
277
        'RATE' => [60, -1, 1, 0],
278
        'MIRR' => [61, 3, 0, 0],
279
        'IRR' => [62, -1, 0, 0],
280
        'RAND' => [63, 0, 1, 1],
281
        'MATCH' => [64, -1, 0, 0],
282
        'DATE' => [65, 3, 1, 0],
283
        'TIME' => [66, 3, 1, 0],
284
        'DAY' => [67, 1, 1, 0],
285
        'MONTH' => [68, 1, 1, 0],
286
        'YEAR' => [69, 1, 1, 0],
287
        'WEEKDAY' => [70, -1, 1, 0],
288
        'HOUR' => [71, 1, 1, 0],
289
        'MINUTE' => [72, 1, 1, 0],
290
        'SECOND' => [73, 1, 1, 0],
291
        'NOW' => [74, 0, 1, 1],
292
        'AREAS' => [75, 1, 0, 1],
293
        'ROWS' => [76, 1, 0, 1],
294
        'COLUMNS' => [77, 1, 0, 1],
295
        'OFFSET' => [78, -1, 0, 1],
296
        'SEARCH' => [82, -1, 1, 0],
297
        'TRANSPOSE' => [83, 1, 1, 0],
298
        'TYPE' => [86, 1, 1, 0],
299
        'ATAN2' => [97, 2, 1, 0],
300
        'ASIN' => [98, 1, 1, 0],
301
        'ACOS' => [99, 1, 1, 0],
302
        'CHOOSE' => [100, -1, 1, 0],
303
        'HLOOKUP' => [101, -1, 0, 0],
304
        'VLOOKUP' => [102, -1, 0, 0],
305
        'ISREF' => [105, 1, 0, 0],
306
        'LOG' => [109, -1, 1, 0],
307
        'CHAR' => [111, 1, 1, 0],
308
        'LOWER' => [112, 1, 1, 0],
309
        'UPPER' => [113, 1, 1, 0],
310
        'PROPER' => [114, 1, 1, 0],
311
        'LEFT' => [115, -1, 1, 0],
312
        'RIGHT' => [116, -1, 1, 0],
313
        'EXACT' => [117, 2, 1, 0],
314
        'TRIM' => [118, 1, 1, 0],
315
        'REPLACE' => [119, 4, 1, 0],
316
        'SUBSTITUTE' => [120, -1, 1, 0],
317
        'CODE' => [121, 1, 1, 0],
318
        'FIND' => [124, -1, 1, 0],
319
        'CELL' => [125, -1, 0, 1],
320
        'ISERR' => [126, 1, 1, 0],
321
        'ISTEXT' => [127, 1, 1, 0],
322
        'ISNUMBER' => [128, 1, 1, 0],
323
        'ISBLANK' => [129, 1, 1, 0],
324
        'T' => [130, 1, 0, 0],
325
        'N' => [131, 1, 0, 0],
326
        'DATEVALUE' => [140, 1, 1, 0],
327
        'TIMEVALUE' => [141, 1, 1, 0],
328
        'SLN' => [142, 3, 1, 0],
329
        'SYD' => [143, 4, 1, 0],
330
        'DDB' => [144, -1, 1, 0],
331
        'INDIRECT' => [148, -1, 1, 1],
332
        'CALL' => [150, -1, 1, 0],
333
        'CLEAN' => [162, 1, 1, 0],
334
        'MDETERM' => [163, 1, 2, 0],
335
        'MINVERSE' => [164, 1, 2, 0],
336
        'MMULT' => [165, 2, 2, 0],
337
        'IPMT' => [167, -1, 1, 0],
338
        'PPMT' => [168, -1, 1, 0],
339
        'COUNTA' => [169, -1, 0, 0],
340
        'PRODUCT' => [183, -1, 0, 0],
341
        'FACT' => [184, 1, 1, 0],
342
        'DPRODUCT' => [189, 3, 0, 0],
343
        'ISNONTEXT' => [190, 1, 1, 0],
344
        'STDEVP' => [193, -1, 0, 0],
345
        'VARP' => [194, -1, 0, 0],
346
        'DSTDEVP' => [195, 3, 0, 0],
347
        'DVARP' => [196, 3, 0, 0],
348
        'TRUNC' => [197, -1, 1, 0],
349
        'ISLOGICAL' => [198, 1, 1, 0],
350
        'DCOUNTA' => [199, 3, 0, 0],
351
        'USDOLLAR' => [204, -1, 1, 0],
352
        'FINDB' => [205, -1, 1, 0],
353
        'SEARCHB' => [206, -1, 1, 0],
354
        'REPLACEB' => [207, 4, 1, 0],
355
        'LEFTB' => [208, -1, 1, 0],
356
        'RIGHTB' => [209, -1, 1, 0],
357
        'MIDB' => [210, 3, 1, 0],
358
        'LENB' => [211, 1, 1, 0],
359
        'ROUNDUP' => [212, 2, 1, 0],
360
        'ROUNDDOWN' => [213, 2, 1, 0],
361
        'ASC' => [214, 1, 1, 0],
362
        'DBCS' => [215, 1, 1, 0],
363
        'RANK' => [216, -1, 0, 0],
364
        'ADDRESS' => [219, -1, 1, 0],
365
        'DAYS360' => [220, -1, 1, 0],
366
        'TODAY' => [221, 0, 1, 1],
367
        'VDB' => [222, -1, 1, 0],
368
        'MEDIAN' => [227, -1, 0, 0],
369
        'SUMPRODUCT' => [228, -1, 2, 0],
370
        'SINH' => [229, 1, 1, 0],
371
        'COSH' => [230, 1, 1, 0],
372
        'TANH' => [231, 1, 1, 0],
373
        'ASINH' => [232, 1, 1, 0],
374
        'ACOSH' => [233, 1, 1, 0],
375
        'ATANH' => [234, 1, 1, 0],
376
        'DGET' => [235, 3, 0, 0],
377
        'INFO' => [244, 1, 1, 1],
378
        'DB' => [247, -1, 1, 0],
379
        'FREQUENCY' => [252, 2, 0, 0],
380
        'ERROR.TYPE' => [261, 1, 1, 0],
381
        'REGISTER.ID' => [267, -1, 1, 0],
382
        'AVEDEV' => [269, -1, 0, 0],
383
        'BETADIST' => [270, -1, 1, 0],
384
        'GAMMALN' => [271, 1, 1, 0],
385
        'BETAINV' => [272, -1, 1, 0],
386
        'BINOMDIST' => [273, 4, 1, 0],
387
        'CHIDIST' => [274, 2, 1, 0],
388
        'CHIINV' => [275, 2, 1, 0],
389
        'COMBIN' => [276, 2, 1, 0],
390
        'CONFIDENCE' => [277, 3, 1, 0],
391
        'CRITBINOM' => [278, 3, 1, 0],
392
        'EVEN' => [279, 1, 1, 0],
393
        'EXPONDIST' => [280, 3, 1, 0],
394
        'FDIST' => [281, 3, 1, 0],
395
        'FINV' => [282, 3, 1, 0],
396
        'FISHER' => [283, 1, 1, 0],
397
        'FISHERINV' => [284, 1, 1, 0],
398
        'FLOOR' => [285, 2, 1, 0],
399
        'GAMMADIST' => [286, 4, 1, 0],
400
        'GAMMAINV' => [287, 3, 1, 0],
401
        'CEILING' => [288, 2, 1, 0],
402
        'HYPGEOMDIST' => [289, 4, 1, 0],
403
        'LOGNORMDIST' => [290, 3, 1, 0],
404
        'LOGINV' => [291, 3, 1, 0],
405
        'NEGBINOMDIST' => [292, 3, 1, 0],
406
        'NORMDIST' => [293, 4, 1, 0],
407
        'NORMSDIST' => [294, 1, 1, 0],
408
        'NORMINV' => [295, 3, 1, 0],
409
        'NORMSINV' => [296, 1, 1, 0],
410
        'STANDARDIZE' => [297, 3, 1, 0],
411
        'ODD' => [298, 1, 1, 0],
412
        'PERMUT' => [299, 2, 1, 0],
413
        'POISSON' => [300, 3, 1, 0],
414
        'TDIST' => [301, 3, 1, 0],
415
        'WEIBULL' => [302, 4, 1, 0],
416
        'SUMXMY2' => [303, 2, 2, 0],
417
        'SUMX2MY2' => [304, 2, 2, 0],
418
        'SUMX2PY2' => [305, 2, 2, 0],
419
        'CHITEST' => [306, 2, 2, 0],
420
        'CORREL' => [307, 2, 2, 0],
421
        'COVAR' => [308, 2, 2, 0],
422
        'FORECAST' => [309, 3, 2, 0],
423
        'FTEST' => [310, 2, 2, 0],
424
        'INTERCEPT' => [311, 2, 2, 0],
425
        'PEARSON' => [312, 2, 2, 0],
426
        'RSQ' => [313, 2, 2, 0],
427
        'STEYX' => [314, 2, 2, 0],
428
        'SLOPE' => [315, 2, 2, 0],
429
        'TTEST' => [316, 4, 2, 0],
430
        'PROB' => [317, -1, 2, 0],
431
        'DEVSQ' => [318, -1, 0, 0],
432
        'GEOMEAN' => [319, -1, 0, 0],
433
        'HARMEAN' => [320, -1, 0, 0],
434
        'SUMSQ' => [321, -1, 0, 0],
435
        'KURT' => [322, -1, 0, 0],
436
        'SKEW' => [323, -1, 0, 0],
437
        'ZTEST' => [324, -1, 0, 0],
438
        'LARGE' => [325, 2, 0, 0],
439
        'SMALL' => [326, 2, 0, 0],
440
        'QUARTILE' => [327, 2, 0, 0],
441
        'PERCENTILE' => [328, 2, 0, 0],
442
        'PERCENTRANK' => [329, -1, 0, 0],
443
        'MODE' => [330, -1, 2, 0],
444
        'TRIMMEAN' => [331, 2, 0, 0],
445
        'TINV' => [332, 2, 1, 0],
446
        'CONCATENATE' => [336, -1, 1, 0],
447
        'POWER' => [337, 2, 1, 0],
448
        'RADIANS' => [342, 1, 1, 0],
449
        'DEGREES' => [343, 1, 1, 0],
450
        'SUBTOTAL' => [344, -1, 0, 0],
451
        'SUMIF' => [345, -1, 0, 0],
452
        'COUNTIF' => [346, 2, 0, 0],
453
        'COUNTBLANK' => [347, 1, 0, 0],
454
        'ISPMT' => [350, 4, 1, 0],
455
        'DATEDIF' => [351, 3, 1, 0],
456
        'DATESTRING' => [352, 1, 1, 0],
457
        'NUMBERSTRING' => [353, 2, 1, 0],
458
        'ROMAN' => [354, -1, 1, 0],
459
        'GETPIVOTDATA' => [358, -1, 0, 0],
460
        'HYPERLINK' => [359, -1, 1, 0],
461
        'PHONETIC' => [360, 1, 0, 0],
462
        'AVERAGEA' => [361, -1, 0, 0],
463
        'MAXA' => [362, -1, 0, 0],
464
        'MINA' => [363, -1, 0, 0],
465
        'STDEVPA' => [364, -1, 0, 0],
466
        'VARPA' => [365, -1, 0, 0],
467
        'STDEVA' => [366, -1, 0, 0],
468
        'VARA' => [367, -1, 0, 0],
469
        'BAHTTEXT' => [368, 1, 0, 0],
470
    ];
471
472
    private $spreadsheet;
473
474
    /**
475
     * The class constructor.
476
     */
477 65
    public function __construct(Spreadsheet $spreadsheet)
478
    {
479 65
        $this->spreadsheet = $spreadsheet;
480
481 65
        $this->currentCharacter = 0;
482 65
        $this->currentToken = ''; // The token we are working on.
483 65
        $this->formula = ''; // The formula to parse.
484 65
        $this->lookAhead = ''; // The character ahead of the current char.
485 65
        $this->parseTree = ''; // The parse tree to be generated.
486 65
        $this->externalSheets = [];
487 65
        $this->references = [];
488 65
    }
489
490
    /**
491
     * Convert a token to the proper ptg value.
492
     *
493
     * @param mixed $token the token to convert
494
     *
495
     * @return mixed the converted token on success
496
     */
497 23
    private function convert($token)
498
    {
499 23
        if (preg_match('/"([^"]|""){0,255}"/', $token)) {
500 9
            return $this->convertString($token);
501 23
        } elseif (is_numeric($token)) {
502 20
            return $this->convertNumber($token);
503
        // match references like A1 or $A$1
504 23
        } elseif (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) {
505 13
            return $this->convertRef2d($token);
506
        // match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1
507 23
        } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) {
508
            return $this->convertRef3d($token);
509
        // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1
510 23
        } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) {
511 4
            return $this->convertRef3d($token);
512
        // match ranges like A1:B2 or $A$1:$B$2
513 23
        } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) {
514 17
            return $this->convertRange2d($token);
515
        // 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
516 22
        } 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)) {
517
            return $this->convertRange3d($token);
518
        // 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
519 22
        } 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)) {
520 4
            return $this->convertRange3d($token);
521
        // operators (including parentheses)
522 21
        } elseif (isset($this->ptg[$token])) {
523 13
            return pack('C', $this->ptg[$token]);
524
        // match error codes
525 20
        } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') {
526
            return $this->convertError($token);
527 20
        } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) {
528 4
            return $this->convertDefinedName($token);
529
        // commented so argument number can be processed correctly. See toReversePolish().
530
            /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token))
531
            {
532
                return($this->convertFunction($token, $this->_func_args));
533
            }*/
534
            // if it's an argument, ignore the token (the argument remains)
535 18
        } elseif ($token == 'arg') {
536 18
            return '';
537
        }
538
539
        // TODO: use real error codes
540
        throw new WriterException("Unknown token $token");
541
    }
542
543
    /**
544
     * Convert a number token to ptgInt or ptgNum.
545
     *
546
     * @param mixed $num an integer or double for conversion to its ptg value
547
     *
548
     * @return string
549
     */
550 20
    private function convertNumber($num)
551
    {
552
        // Integer in the range 0..2**16-1
553 20
        if ((preg_match('/^\\d+$/', $num)) && ($num <= 65535)) {
554 20
            return pack('Cv', $this->ptg['ptgInt'], $num);
555
        }
556
557
        // A float
558 6
        if (BIFFwriter::getByteOrder()) { // if it's Big Endian
559
            $num = strrev($num);
560
        }
561
562 6
        return pack('Cd', $this->ptg['ptgNum'], $num);
563
    }
564
565
    /**
566
     * Convert a string token to ptgStr.
567
     *
568
     * @param string $string a string for conversion to its ptg value
569
     *
570
     * @return mixed the converted token on success
571
     */
572 9
    private function convertString($string)
573
    {
574
        // chop away beggining and ending quotes
575 9
        $string = substr($string, 1, -1);
576 9
        if (strlen($string) > 255) {
577
            throw new WriterException('String is too long');
578
        }
579
580 9
        return pack('C', $this->ptg['ptgStr']) . StringHelper::UTF8toBIFF8UnicodeShort($string);
581
    }
582
583
    /**
584
     * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of
585
     * args that it takes.
586
     *
587
     * @param string $token the name of the function for convertion to ptg value
588
     * @param int $num_args the number of arguments the function receives
589
     *
590
     * @return string The packed ptg for the function
591
     */
592 18
    private function convertFunction($token, $num_args)
593
    {
594 18
        $args = $this->functions[$token][1];
595
596
        // Fixed number of args eg. TIME($i, $j, $k).
597 18
        if ($args >= 0) {
598 5
            return pack('Cv', $this->ptg['ptgFuncV'], $this->functions[$token][0]);
599
        }
600
601
        // Variable number of args eg. SUM($i, $j, $k, ..).
602 17
        return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]);
603
    }
604
605
    /**
606
     * Convert an Excel range such as A1:D4 to a ptgRefV.
607
     *
608
     * @param string $range An Excel range in the A1:A2
609
     * @param int $class
610
     *
611
     * @return string
612
     */
613 17
    private function convertRange2d($range, $class = 0)
614
    {
615
        // TODO: possible class value 0,1,2 check Formula.pm
616
        // Split the range into 2 cell refs
617 17
        if (preg_match('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) {
618 17
            [$cell1, $cell2] = explode(':', $range);
619
        } else {
620
            // TODO: use real error codes
621
            throw new WriterException('Unknown range separator');
622
        }
623
624
        // Convert the cell references
625 17
        [$row1, $col1] = $this->cellToPackedRowcol($cell1);
626 17
        [$row2, $col2] = $this->cellToPackedRowcol($cell2);
627
628
        // The ptg value depends on the class of the ptg.
629 17
        if ($class == 0) {
630 17
            $ptgArea = pack('C', $this->ptg['ptgArea']);
631
        } elseif ($class == 1) {
632
            $ptgArea = pack('C', $this->ptg['ptgAreaV']);
633
        } elseif ($class == 2) {
634
            $ptgArea = pack('C', $this->ptg['ptgAreaA']);
635
        } else {
636
            // TODO: use real error codes
637
            throw new WriterException("Unknown class $class");
638
        }
639
640 17
        return $ptgArea . $row1 . $row2 . $col1 . $col2;
641
    }
642
643
    /**
644
     * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to
645
     * a ptgArea3d.
646
     *
647
     * @param string $token an Excel range in the Sheet1!A1:A2 format
648
     *
649
     * @return mixed the packed ptgArea3d token on success
650
     */
651 4
    private function convertRange3d($token)
652
    {
653
        // Split the ref at the ! symbol
654 4
        [$ext_ref, $range] = PhpspreadsheetWorksheet::extractSheetTitle($token, true);
655
656
        // Convert the external reference part (different for BIFF8)
657 4
        $ext_ref = $this->getRefIndex($ext_ref);
658
659
        // Split the range into 2 cell refs
660 4
        [$cell1, $cell2] = explode(':', $range);
661
662
        // Convert the cell references
663 4
        if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\\d+)$/', $cell1)) {
664 4
            [$row1, $col1] = $this->cellToPackedRowcol($cell1);
665 4
            [$row2, $col2] = $this->cellToPackedRowcol($cell2);
666
        } else { // It's a rows range (like 26:27)
667
            [$row1, $col1, $row2, $col2] = $this->rangeToPackedRange($cell1 . ':' . $cell2);
668
        }
669
670
        // The ptg value depends on the class of the ptg.
671 4
        $ptgArea = pack('C', $this->ptg['ptgArea3d']);
672
673 4
        return $ptgArea . $ext_ref . $row1 . $row2 . $col1 . $col2;
674
    }
675
676
    /**
677
     * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV.
678
     *
679
     * @param string $cell An Excel cell reference
680
     *
681
     * @return string The cell in packed() format with the corresponding ptg
682
     */
683 13
    private function convertRef2d($cell)
684
    {
685
        // Convert the cell reference
686 13
        $cell_array = $this->cellToPackedRowcol($cell);
687 13
        [$row, $col] = $cell_array;
688
689
        // The ptg value depends on the class of the ptg.
690 13
        $ptgRef = pack('C', $this->ptg['ptgRefA']);
691
692 13
        return $ptgRef . $row . $col;
693
    }
694
695
    /**
696
     * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a
697
     * ptgRef3d.
698
     *
699
     * @param string $cell An Excel cell reference
700
     *
701
     * @return mixed the packed ptgRef3d token on success
702
     */
703 4
    private function convertRef3d($cell)
704
    {
705
        // Split the ref at the ! symbol
706 4
        [$ext_ref, $cell] = PhpspreadsheetWorksheet::extractSheetTitle($cell, true);
707
708
        // Convert the external reference part (different for BIFF8)
709 4
        $ext_ref = $this->getRefIndex($ext_ref);
710
711
        // Convert the cell reference part
712 4
        [$row, $col] = $this->cellToPackedRowcol($cell);
713
714
        // The ptg value depends on the class of the ptg.
715 4
        $ptgRef = pack('C', $this->ptg['ptgRef3dA']);
716
717 4
        return $ptgRef . $ext_ref . $row . $col;
718
    }
719
720
    /**
721
     * Convert an error code to a ptgErr.
722
     *
723
     * @param string $errorCode The error code for conversion to its ptg value
724
     *
725
     * @return string The error code ptgErr
726
     */
727
    private function convertError($errorCode)
728
    {
729
        switch ($errorCode) {
730
            case '#NULL!':
731
                return pack('C', 0x00);
732
            case '#DIV/0!':
733
                return pack('C', 0x07);
734
            case '#VALUE!':
735
                return pack('C', 0x0F);
736
            case '#REF!':
737
                return pack('C', 0x17);
738
            case '#NAME?':
739
                return pack('C', 0x1D);
740
            case '#NUM!':
741
                return pack('C', 0x24);
742
            case '#N/A':
743
                return pack('C', 0x2A);
744
        }
745
746
        return pack('C', 0xFF);
747
    }
748
749 4
    private function convertDefinedName(string $name): string
750
    {
751 4
        if (strlen($name) > 255) {
752
            throw new WriterException('Defined Name is too long');
753
        }
754
755 4
        $nameReference = 1;
756 4
        foreach ($this->spreadsheet->getDefinedNames() as $definedName) {
757 4
            if ($name === $definedName->getName()) {
758 4
                break;
759
            }
760 3
            ++$nameReference;
761
        }
762
763 4
        $ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference);
0 ignored issues
show
Unused Code introduced by
The assignment to $ptgRef is dead and can be removed.
Loading history...
764
765 4
        throw new WriterException('Cannot yet write formulae with defined names to Xls');
766
767
        return $ptgRef;
0 ignored issues
show
Unused Code introduced by
return $ptgRef is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
768
    }
769
770
    /**
771
     * Look up the REF index that corresponds to an external sheet name
772
     * (or range). If it doesn't exist yet add it to the workbook's references
773
     * array. It assumes all sheet names given must exist.
774
     *
775
     * @param string $ext_ref The name of the external reference
776
     *
777
     * @return mixed The reference index in packed() format on success
778
     */
779 6
    private function getRefIndex($ext_ref)
780
    {
781 6
        $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading  ' if any.
782 6
        $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any.
783 6
        $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with '
784
785
        // Check if there is a sheet range eg., Sheet1:Sheet2.
786 6
        if (preg_match('/:/', $ext_ref)) {
787
            [$sheet_name1, $sheet_name2] = explode(':', $ext_ref);
788
789
            $sheet1 = $this->getSheetIndex($sheet_name1);
790
            if ($sheet1 == -1) {
791
                throw new WriterException("Unknown sheet name $sheet_name1 in formula");
792
            }
793
            $sheet2 = $this->getSheetIndex($sheet_name2);
794
            if ($sheet2 == -1) {
795
                throw new WriterException("Unknown sheet name $sheet_name2 in formula");
796
            }
797
798
            // Reverse max and min sheet numbers if necessary
799
            if ($sheet1 > $sheet2) {
800
                [$sheet1, $sheet2] = [$sheet2, $sheet1];
801
            }
802
        } else { // Single sheet name only.
803 6
            $sheet1 = $this->getSheetIndex($ext_ref);
804 6
            if ($sheet1 == -1) {
805
                throw new WriterException("Unknown sheet name $ext_ref in formula");
806
            }
807 6
            $sheet2 = $sheet1;
808
        }
809
810
        // assume all references belong to this document
811 6
        $supbook_index = 0x00;
812 6
        $ref = pack('vvv', $supbook_index, $sheet1, $sheet2);
813 6
        $totalreferences = count($this->references);
814 6
        $index = -1;
815 6
        for ($i = 0; $i < $totalreferences; ++$i) {
816 6
            if ($ref == $this->references[$i]) {
817 6
                $index = $i;
818
819 6
                break;
820
            }
821
        }
822
        // if REF was not found add it to references array
823 6
        if ($index == -1) {
824
            $this->references[$totalreferences] = $ref;
825
            $index = $totalreferences;
826
        }
827
828 6
        return pack('v', $index);
829
    }
830
831
    /**
832
     * Look up the index that corresponds to an external sheet name. The hash of
833
     * sheet names is updated by the addworksheet() method of the
834
     * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class.
835
     *
836
     * @param string $sheet_name Sheet name
837
     *
838
     * @return int The sheet index, -1 if the sheet was not found
839
     */
840 6
    private function getSheetIndex($sheet_name)
841
    {
842 6
        if (!isset($this->externalSheets[$sheet_name])) {
843
            return -1;
844
        }
845
846 6
        return $this->externalSheets[$sheet_name];
847
    }
848
849
    /**
850
     * This method is used to update the array of sheet names. It is
851
     * called by the addWorksheet() method of the
852
     * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class.
853
     *
854
     * @param string $name The name of the worksheet being added
855
     * @param int $index The index of the worksheet being added
856
     *
857
     * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet()
858
     */
859 64
    public function setExtSheet($name, $index): void
860
    {
861 64
        $this->externalSheets[$name] = $index;
862 64
    }
863
864
    /**
865
     * pack() row and column into the required 3 or 4 byte format.
866
     *
867
     * @param string $cell The Excel cell reference to be packed
868
     *
869
     * @return array Array containing the row and column in packed() format
870
     */
871 21
    private function cellToPackedRowcol($cell)
872
    {
873 21
        $cell = strtoupper($cell);
874 21
        [$row, $col, $row_rel, $col_rel] = $this->cellToRowcol($cell);
875 21
        if ($col >= 256) {
876
            throw new WriterException("Column in: $cell greater than 255");
877
        }
878 21
        if ($row >= 65536) {
879
            throw new WriterException("Row in: $cell greater than 65536 ");
880
        }
881
882
        // Set the high bits to indicate if row or col are relative.
883 21
        $col |= $col_rel << 14;
884 21
        $col |= $row_rel << 15;
885 21
        $col = pack('v', $col);
886
887 21
        $row = pack('v', $row);
888
889 21
        return [$row, $col];
890
    }
891
892
    /**
893
     * pack() row range into the required 3 or 4 byte format.
894
     * Just using maximum col/rows, which is probably not the correct solution.
895
     *
896
     * @param string $range The Excel range to be packed
897
     *
898
     * @return array Array containing (row1,col1,row2,col2) in packed() format
899
     */
900
    private function rangeToPackedRange($range)
901
    {
902
        preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match);
903
        // return absolute rows if there is a $ in the ref
904
        $row1_rel = empty($match[1]) ? 1 : 0;
905
        $row1 = $match[2];
906
        $row2_rel = empty($match[3]) ? 1 : 0;
907
        $row2 = $match[4];
908
        // Convert 1-index to zero-index
909
        --$row1;
910
        --$row2;
911
        // Trick poor inocent Excel
912
        $col1 = 0;
913
        $col2 = 65535; // FIXME: maximum possible value for Excel 5 (change this!!!)
914
915
        // FIXME: this changes for BIFF8
916
        if (($row1 >= 65536) || ($row2 >= 65536)) {
917
            throw new WriterException("Row in: $range greater than 65536 ");
918
        }
919
920
        // Set the high bits to indicate if rows are relative.
921
        $col1 |= $row1_rel << 15;
922
        $col2 |= $row2_rel << 15;
923
        $col1 = pack('v', $col1);
924
        $col2 = pack('v', $col2);
925
926
        $row1 = pack('v', $row1);
927
        $row2 = pack('v', $row2);
928
929
        return [$row1, $col1, $row2, $col2];
930
    }
931
932
    /**
933
     * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero
934
     * indexed row and column number. Also returns two (0,1) values to indicate
935
     * whether the row or column are relative references.
936
     *
937
     * @param string $cell the Excel cell reference in A1 format
938
     *
939
     * @return array
940
     */
941 21
    private function cellToRowcol($cell)
942
    {
943 21
        preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match);
944
        // return absolute column if there is a $ in the ref
945 21
        $col_rel = empty($match[1]) ? 1 : 0;
946 21
        $col_ref = $match[2];
947 21
        $row_rel = empty($match[3]) ? 1 : 0;
948 21
        $row = $match[4];
949
950
        // Convert base26 column string to a number.
951 21
        $expn = strlen($col_ref) - 1;
952 21
        $col = 0;
953 21
        $col_ref_length = strlen($col_ref);
954 21
        for ($i = 0; $i < $col_ref_length; ++$i) {
955 21
            $col += (ord($col_ref[$i]) - 64) * 26 ** $expn;
956 21
            --$expn;
957
        }
958
959
        // Convert 1-index to zero-index
960 21
        --$row;
961 21
        --$col;
962
963 21
        return [$row, $col, $row_rel, $col_rel];
964
    }
965
966
    /**
967
     * Advance to the next valid token.
968
     */
969 23
    private function advance()
970
    {
971 23
        $token = '';
972 23
        $i = $this->currentCharacter;
973 23
        $formula_length = strlen($this->formula);
974
        // eat up white spaces
975 23
        if ($i < $formula_length) {
976 23
            while ($this->formula[$i] == ' ') {
977 2
                ++$i;
978
            }
979
980 23
            if ($i < ($formula_length - 1)) {
981 23
                $this->lookAhead = $this->formula[$i + 1];
982
            }
983 23
            $token = '';
984
        }
985
986 23
        while ($i < $formula_length) {
987 23
            $token .= $this->formula[$i];
988
989 23
            if ($i < ($formula_length - 1)) {
990 23
                $this->lookAhead = $this->formula[$i + 1];
991
            } else {
992 23
                $this->lookAhead = '';
993
            }
994
995 23
            if ($this->match($token) != '') {
996 23
                $this->currentCharacter = $i + 1;
997 23
                $this->currentToken = $token;
998
999 23
                return 1;
1000
            }
1001
1002 23
            if ($i < ($formula_length - 2)) {
1003 23
                $this->lookAhead = $this->formula[$i + 2];
1004
            } else { // if we run out of characters lookAhead becomes empty
1005 16
                $this->lookAhead = '';
1006
            }
1007 23
            ++$i;
1008
        }
1009
        //die("Lexical error ".$this->currentCharacter);
1010 23
    }
1011
1012
    /**
1013
     * Checks if it's a valid token.
1014
     *
1015
     * @param mixed $token the token to check
1016
     *
1017
     * @return mixed The checked token or false on failure
1018
     */
1019 23
    private function match($token)
1020
    {
1021
        switch ($token) {
1022 23
            case '+':
1023 23
            case '-':
1024 23
            case '*':
1025 23
            case '/':
1026 23
            case '(':
1027 23
            case ')':
1028 23
            case ',':
1029 23
            case ';':
1030 23
            case '>=':
1031 23
            case '<=':
1032 23
            case '=':
1033 23
            case '<>':
1034 23
            case '^':
1035 23
            case '&':
1036 23
            case '%':
1037 22
                return $token;
1038
1039
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
1040 23
            case '>':
1041
                if ($this->lookAhead === '=') { // it's a GE token
1042
                    break;
1043
                }
1044
1045
                return $token;
1046
1047
                break;
1048 23
            case '<':
1049
                // it's a LE or a NE token
1050 5
                if (($this->lookAhead === '=') || ($this->lookAhead === '>')) {
1051 5
                    break;
1052
                }
1053
1054
                return $token;
1055
1056
                break;
1057
            default:
1058
                // if it's a reference A1 or $A$1 or $A1 or A$1
1059 23
                if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) {
1060 13
                    return $token;
1061 23
                } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) {
1062
                    // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
1063
                    return $token;
1064 23
                } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) {
1065
                    // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
1066 4
                    return $token;
1067 23
                } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) {
1068
                    // if it's a range A1:A2 or $A$1:$A$2
1069 17
                    return $token;
1070 23
                } 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) && !preg_match('/\d/', $this->lookAhead)) {
1071
                    // 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
1072
                    return $token;
1073 23
                } 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) && !preg_match('/\d/', $this->lookAhead)) {
1074
                    // 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
1075 4
                    return $token;
1076 23
                } elseif (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) {
1077
                    // If it's a number (check that it's not a sheet name or range)
1078 12
                    return $token;
1079 23
                } elseif (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) {
1080
                    // If it's a string (of maximum 255 characters)
1081 10
                    return $token;
1082 23
                } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') {
1083
                    // If it's an error code
1084
                    return $token;
1085 23
                } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) {
1086
                    // if it's a function call
1087 19
                    return $token;
1088 23
                } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) {
1089 4
                    return $token;
1090 23
                } elseif (substr($token, -1) === ')') {
1091
                    //    It's an argument of some description (e.g. a named range),
1092
                    //        precise nature yet to be determined
1093 1
                    return $token;
1094
                }
1095
1096 23
                return '';
1097
        }
1098 5
    }
1099
1100
    /**
1101
     * The parsing method. It parses a formula.
1102
     *
1103
     * @param string $formula the formula to parse, without the initial equal
1104
     *                        sign (=)
1105
     *
1106
     * @return mixed true on success
1107
     */
1108 23
    public function parse($formula)
1109
    {
1110 23
        $this->currentCharacter = 0;
1111 23
        $this->formula = (string) $formula;
1112 23
        $this->lookAhead = $formula[1] ?? '';
1113 23
        $this->advance();
1114 23
        $this->parseTree = $this->condition();
1115
1116 23
        return true;
1117
    }
1118
1119
    /**
1120
     * It parses a condition. It assumes the following rule:
1121
     * Cond -> Expr [(">" | "<") Expr].
1122
     *
1123
     * @return mixed The parsed ptg'd tree on success
1124
     */
1125 23
    private function condition()
1126
    {
1127 23
        $result = $this->expression();
1128 23
        if ($this->currentToken == '<') {
1129
            $this->advance();
1130
            $result2 = $this->expression();
1131
            $result = $this->createTree('ptgLT', $result, $result2);
1132 23
        } elseif ($this->currentToken == '>') {
1133
            $this->advance();
1134
            $result2 = $this->expression();
1135
            $result = $this->createTree('ptgGT', $result, $result2);
1136 23
        } elseif ($this->currentToken == '<=') {
1137
            $this->advance();
1138
            $result2 = $this->expression();
1139
            $result = $this->createTree('ptgLE', $result, $result2);
1140 23
        } elseif ($this->currentToken == '>=') {
1141
            $this->advance();
1142
            $result2 = $this->expression();
1143
            $result = $this->createTree('ptgGE', $result, $result2);
1144 23
        } elseif ($this->currentToken == '=') {
1145 1
            $this->advance();
1146 1
            $result2 = $this->expression();
1147 1
            $result = $this->createTree('ptgEQ', $result, $result2);
1148 23
        } elseif ($this->currentToken == '<>') {
1149 5
            $this->advance();
1150 5
            $result2 = $this->expression();
1151 5
            $result = $this->createTree('ptgNE', $result, $result2);
1152 23
        } elseif ($this->currentToken == '&') {
1153 6
            $this->advance();
1154 6
            $result2 = $this->expression();
1155 6
            $result = $this->createTree('ptgConcat', $result, $result2);
1156
        }
1157
1158 23
        return $result;
1159
    }
1160
1161
    /**
1162
     * It parses a expression. It assumes the following rule:
1163
     * Expr -> Term [("+" | "-") Term]
1164
     *      -> "string"
1165
     *      -> "-" Term : Negative value
1166
     *      -> "+" Term : Positive value
1167
     *      -> Error code.
1168
     *
1169
     * @return mixed The parsed ptg'd tree on success
1170
     */
1171 23
    private function expression()
1172
    {
1173
        // If it's a string return a string node
1174 23
        if (preg_match('/"([^"]|""){0,255}"/', $this->currentToken)) {
1175 10
            $tmp = str_replace('""', '"', $this->currentToken);
1176 10
            if (($tmp == '"') || ($tmp == '')) {
1177
                //    Trap for "" that has been used for an empty string
1178 5
                $tmp = '""';
1179
            }
1180 10
            $result = $this->createTree($tmp, '', '');
1181 10
            $this->advance();
1182
1183 10
            return $result;
1184
        // If it's an error code
1185 23
        } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) || $this->currentToken == '#N/A') {
1186
            $result = $this->createTree($this->currentToken, 'ptgErr', '');
1187
            $this->advance();
1188
1189
            return $result;
1190
        // If it's a negative value
1191 23
        } elseif ($this->currentToken == '-') {
1192
            // catch "-" Term
1193
            $this->advance();
1194
            $result2 = $this->expression();
1195
1196
            return $this->createTree('ptgUminus', $result2, '');
1197
        // If it's a positive value
1198 23
        } elseif ($this->currentToken == '+') {
1199
            // catch "+" Term
1200
            $this->advance();
1201
            $result2 = $this->expression();
1202
1203
            return $this->createTree('ptgUplus', $result2, '');
1204
        }
1205 23
        $result = $this->term();
1206
        while (
1207 23
            ($this->currentToken == '+') ||
1208 23
            ($this->currentToken == '-') ||
1209 23
            ($this->currentToken == '^')
1210
        ) {
1211 10
            if ($this->currentToken == '+') {
1212 10
                $this->advance();
1213 10
                $result2 = $this->term();
1214 10
                $result = $this->createTree('ptgAdd', $result, $result2);
1215 3
            } elseif ($this->currentToken == '-') {
1216 3
                $this->advance();
1217 3
                $result2 = $this->term();
1218 3
                $result = $this->createTree('ptgSub', $result, $result2);
1219
            } else {
1220
                $this->advance();
1221
                $result2 = $this->term();
1222
                $result = $this->createTree('ptgPower', $result, $result2);
1223
            }
1224
        }
1225
1226 23
        return $result;
1227
    }
1228
1229
    /**
1230
     * This function just introduces a ptgParen element in the tree, so that Excel
1231
     * doesn't get confused when working with a parenthesized formula afterwards.
1232
     *
1233
     * @return array The parsed ptg'd tree
1234
     *
1235
     * @see fact()
1236
     */
1237 1
    private function parenthesizedExpression()
1238
    {
1239 1
        return $this->createTree('ptgParen', $this->expression(), '');
1240
    }
1241
1242
    /**
1243
     * It parses a term. It assumes the following rule:
1244
     * Term -> Fact [("*" | "/") Fact].
1245
     *
1246
     * @return mixed The parsed ptg'd tree on success
1247
     */
1248 23
    private function term()
1249
    {
1250 23
        $result = $this->fact();
1251
        while (
1252 23
            ($this->currentToken == '*') ||
1253 23
            ($this->currentToken == '/')
1254
        ) {
1255 12
            if ($this->currentToken == '*') {
1256 12
                $this->advance();
1257 12
                $result2 = $this->fact();
1258 12
                $result = $this->createTree('ptgMul', $result, $result2);
1259
            } else {
1260 2
                $this->advance();
1261 2
                $result2 = $this->fact();
1262 2
                $result = $this->createTree('ptgDiv', $result, $result2);
1263
            }
1264
        }
1265
1266 23
        return $result;
1267
    }
1268
1269
    /**
1270
     * It parses a factor. It assumes the following rule:
1271
     * Fact -> ( Expr )
1272
     *       | CellRef
1273
     *       | CellRange
1274
     *       | Number
1275
     *       | Function.
1276
     *
1277
     * @return mixed The parsed ptg'd tree on success
1278
     */
1279 23
    private function fact()
1280
    {
1281 23
        if ($this->currentToken === '(') {
1282 1
            $this->advance(); // eat the "("
1283 1
            $result = $this->parenthesizedExpression();
1284 1
            if ($this->currentToken !== ')') {
1285
                throw new WriterException("')' token expected.");
1286
            }
1287 1
            $this->advance(); // eat the ")"
1288
1289 1
            return $result;
1290
        }
1291
        // if it's a reference
1292 23
        if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) {
1293 13
            $result = $this->createTree($this->currentToken, '', '');
1294 13
            $this->advance();
1295
1296 13
            return $result;
1297 22
        } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) {
1298
            // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
1299
            $result = $this->createTree($this->currentToken, '', '');
1300
            $this->advance();
1301
1302
            return $result;
1303 22
        } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) {
1304
            // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
1305 4
            $result = $this->createTree($this->currentToken, '', '');
1306 4
            $this->advance();
1307
1308 4
            return $result;
1309
        } elseif (
1310 22
            preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) ||
1311 22
            preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken)
1312
        ) {
1313
            // if it's a range A1:B2 or $A$1:$B$2
1314
            // must be an error?
1315 17
            $result = $this->createTree($this->currentToken, '', '');
1316 17
            $this->advance();
1317
1318 17
            return $result;
1319 22
        } 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)) {
1320
            // 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)
1321
            // must be an error?
1322
            $result = $this->createTree($this->currentToken, '', '');
1323
            $this->advance();
1324
1325
            return $result;
1326 22
        } 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)) {
1327
            // 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)
1328
            // must be an error?
1329 4
            $result = $this->createTree($this->currentToken, '', '');
1330 4
            $this->advance();
1331
1332 4
            return $result;
1333 22
        } elseif (is_numeric($this->currentToken)) {
1334
            // If it's a number or a percent
1335 12
            if ($this->lookAhead === '%') {
1336
                $result = $this->createTree('ptgPercent', $this->currentToken, '');
1337
                $this->advance(); // Skip the percentage operator once we've pre-built that tree
1338
            } else {
1339 12
                $result = $this->createTree($this->currentToken, '', '');
1340
            }
1341 12
            $this->advance();
1342
1343 12
            return $result;
1344 21
        } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) {
1345
            // if it's a function call
1346 19
            return $this->func();
1347 6
        } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) {
1348 4
            $result = $this->createTree('ptgName', $this->currentToken, '');
1349 4
            $this->advance();
1350
1351 4
            return $result;
1352
        }
1353
1354 2
        throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter);
1355
    }
1356
1357
    /**
1358
     * It parses a function call. It assumes the following rule:
1359
     * Func -> ( Expr [,Expr]* ).
1360
     *
1361
     * @return mixed The parsed ptg'd tree on success
1362
     */
1363 19
    private function func()
1364
    {
1365 19
        $num_args = 0; // number of arguments received
1366 19
        $function = strtoupper($this->currentToken);
1367 19
        $result = ''; // initialize result
1368 19
        $this->advance();
1369 19
        $this->advance(); // eat the "("
1370 19
        while ($this->currentToken !== ')') {
1371 19
            if ($num_args > 0) {
1372 8
                if ($this->currentToken === ',' || $this->currentToken === ';') {
1373 8
                    $this->advance(); // eat the "," or ";"
1374
                } else {
1375
                    throw new WriterException("Syntax error: comma expected in function $function, arg #{$num_args}");
1376
                }
1377 8
                $result2 = $this->condition();
1378 8
                $result = $this->createTree('arg', $result, $result2);
1379
            } else { // first argument
1380 19
                $result2 = $this->condition();
1381 19
                $result = $this->createTree('arg', '', $result2);
1382
            }
1383 19
            ++$num_args;
1384
        }
1385 19
        if (!isset($this->functions[$function])) {
1386 2
            throw new WriterException("Function $function() doesn't exist");
1387
        }
1388 18
        $args = $this->functions[$function][1];
1389
        // If fixed number of args eg. TIME($i, $j, $k). Check that the number of args is valid.
1390 18
        if (($args >= 0) && ($args != $num_args)) {
1391
            throw new WriterException("Incorrect number of arguments in function $function() ");
1392
        }
1393
1394 18
        $result = $this->createTree($function, $result, $num_args);
1395 18
        $this->advance(); // eat the ")"
1396
1397 18
        return $result;
1398
    }
1399
1400
    /**
1401
     * Creates a tree. In fact an array which may have one or two arrays (sub-trees)
1402
     * as elements.
1403
     *
1404
     * @param mixed $value the value of this node
1405
     * @param mixed $left the left array (sub-tree) or a final node
1406
     * @param mixed $right the right array (sub-tree) or a final node
1407
     *
1408
     * @return array A tree
1409
     */
1410 23
    private function createTree($value, $left, $right)
1411
    {
1412 23
        return ['value' => $value, 'left' => $left, 'right' => $right];
1413
    }
1414
1415
    /**
1416
     * Builds a string containing the tree in reverse polish notation (What you
1417
     * would use in a HP calculator stack).
1418
     * The following tree:.
1419
     *
1420
     *    +
1421
     *   / \
1422
     *  2   3
1423
     *
1424
     * produces: "23+"
1425
     *
1426
     * The following tree:
1427
     *
1428
     *    +
1429
     *   / \
1430
     *  3   *
1431
     *     / \
1432
     *    6   A1
1433
     *
1434
     * produces: "36A1*+"
1435
     *
1436
     * In fact all operands, functions, references, etc... are written as ptg's
1437
     *
1438
     * @param array $tree the optional tree to convert
1439
     *
1440
     * @return string The tree in reverse polish notation
1441
     */
1442 23
    public function toReversePolish($tree = [])
1443
    {
1444 23
        $polish = ''; // the string we are going to return
1445 23
        if (empty($tree)) { // If it's the first call use parseTree
1446 23
            $tree = $this->parseTree;
1447
        }
1448
1449 23
        if (is_array($tree['left'])) {
1450 21
            $converted_tree = $this->toReversePolish($tree['left']);
1451 21
            $polish .= $converted_tree;
1452 23
        } elseif ($tree['left'] != '') { // It's a final node
1453 4
            $converted_tree = $this->convert($tree['left']);
1454
            $polish .= $converted_tree;
1455
        }
1456 23
        if (is_array($tree['right'])) {
1457 21
            $converted_tree = $this->toReversePolish($tree['right']);
1458 20
            $polish .= $converted_tree;
1459 23
        } elseif ($tree['right'] != '') { // It's a final node
1460 18
            $converted_tree = $this->convert($tree['right']);
1461 18
            $polish .= $converted_tree;
1462
        }
1463
        // if it's a function convert it here (so we can set it's arguments)
1464
        if (
1465 23
            preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/", $tree['value']) &&
1466 23
            !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value']) &&
1467 23
            !preg_match('/^[A-Ia-i]?[A-Za-z](\\d+)\\.\\.[A-Ia-i]?[A-Za-z](\\d+)$/', $tree['value']) &&
1468 23
            !is_numeric($tree['value']) &&
1469 23
            !isset($this->ptg[$tree['value']])
1470
        ) {
1471
            // left subtree for a function is always an array.
1472 18
            if ($tree['left'] != '') {
1473 18
                $left_tree = $this->toReversePolish($tree['left']);
1474
            } else {
1475 4
                $left_tree = '';
1476
            }
1477
1478
            // add it's left subtree and return.
1479 18
            return $left_tree . $this->convertFunction($tree['value'], $tree['right']);
1480
        }
1481 23
        $converted_tree = $this->convert($tree['value']);
1482
1483 23
        return $polish . $converted_tree;
1484
    }
1485
}
1486