Failed Conditions
Push — master ( a2bb82...a189d9 )
by Adrien
10:27 queued 01:00
created

Parser::match()   D

Complexity

Conditions 55
Paths 32

Size

Total Lines 78
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 63.3006

Importance

Changes 0
Metric Value
cc 55
eloc 55
nc 32
nop 1
dl 0
loc 78
ccs 43
cts 50
cp 0.86
crap 63.3006
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        // Variable number of args eg. SUM($i, $j, $k, ..).
601 17
        if ($args == -1) {
602 17
            return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]);
603
        }
604
    }
605
606
    /**
607
     * Convert an Excel range such as A1:D4 to a ptgRefV.
608
     *
609
     * @param string $range An Excel range in the A1:A2
610
     * @param int $class
611
     *
612
     * @return string
613
     */
614 17
    private function convertRange2d($range, $class = 0)
615
    {
616
        // TODO: possible class value 0,1,2 check Formula.pm
617
        // Split the range into 2 cell refs
618 17
        if (preg_match('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) {
619 17
            [$cell1, $cell2] = explode(':', $range);
620
        } else {
621
            // TODO: use real error codes
622
            throw new WriterException('Unknown range separator');
623
        }
624
625
        // Convert the cell references
626 17
        [$row1, $col1] = $this->cellToPackedRowcol($cell1);
627 17
        [$row2, $col2] = $this->cellToPackedRowcol($cell2);
628
629
        // The ptg value depends on the class of the ptg.
630 17
        if ($class == 0) {
631 17
            $ptgArea = pack('C', $this->ptg['ptgArea']);
632
        } elseif ($class == 1) {
633
            $ptgArea = pack('C', $this->ptg['ptgAreaV']);
634
        } elseif ($class == 2) {
635
            $ptgArea = pack('C', $this->ptg['ptgAreaA']);
636
        } else {
637
            // TODO: use real error codes
638
            throw new WriterException("Unknown class $class");
639
        }
640
641 17
        return $ptgArea . $row1 . $row2 . $col1 . $col2;
642
    }
643
644
    /**
645
     * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to
646
     * a ptgArea3d.
647
     *
648
     * @param string $token an Excel range in the Sheet1!A1:A2 format
649
     *
650
     * @return mixed the packed ptgArea3d token on success
651
     */
652 4
    private function convertRange3d($token)
653
    {
654
        // Split the ref at the ! symbol
655 4
        [$ext_ref, $range] = PhpspreadsheetWorksheet::extractSheetTitle($token, true);
656
657
        // Convert the external reference part (different for BIFF8)
658 4
        $ext_ref = $this->getRefIndex($ext_ref);
659
660
        // Split the range into 2 cell refs
661 4
        [$cell1, $cell2] = explode(':', $range);
662
663
        // Convert the cell references
664 4
        if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\\d+)$/', $cell1)) {
665 4
            [$row1, $col1] = $this->cellToPackedRowcol($cell1);
666 4
            [$row2, $col2] = $this->cellToPackedRowcol($cell2);
667
        } else { // It's a rows range (like 26:27)
668
            [$row1, $col1, $row2, $col2] = $this->rangeToPackedRange($cell1 . ':' . $cell2);
669
        }
670
671
        // The ptg value depends on the class of the ptg.
672 4
        $ptgArea = pack('C', $this->ptg['ptgArea3d']);
673
674 4
        return $ptgArea . $ext_ref . $row1 . $row2 . $col1 . $col2;
675
    }
676
677
    /**
678
     * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV.
679
     *
680
     * @param string $cell An Excel cell reference
681
     *
682
     * @return string The cell in packed() format with the corresponding ptg
683
     */
684 13
    private function convertRef2d($cell)
685
    {
686
        // Convert the cell reference
687 13
        $cell_array = $this->cellToPackedRowcol($cell);
688 13
        [$row, $col] = $cell_array;
689
690
        // The ptg value depends on the class of the ptg.
691 13
        $ptgRef = pack('C', $this->ptg['ptgRefA']);
692
693 13
        return $ptgRef . $row . $col;
694
    }
695
696
    /**
697
     * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a
698
     * ptgRef3d.
699
     *
700
     * @param string $cell An Excel cell reference
701
     *
702
     * @return mixed the packed ptgRef3d token on success
703
     */
704 4
    private function convertRef3d($cell)
705
    {
706
        // Split the ref at the ! symbol
707 4
        [$ext_ref, $cell] = PhpspreadsheetWorksheet::extractSheetTitle($cell, true);
708
709
        // Convert the external reference part (different for BIFF8)
710 4
        $ext_ref = $this->getRefIndex($ext_ref);
711
712
        // Convert the cell reference part
713 4
        [$row, $col] = $this->cellToPackedRowcol($cell);
714
715
        // The ptg value depends on the class of the ptg.
716 4
        $ptgRef = pack('C', $this->ptg['ptgRef3dA']);
717
718 4
        return $ptgRef . $ext_ref . $row . $col;
719
    }
720
721
    /**
722
     * Convert an error code to a ptgErr.
723
     *
724
     * @param string $errorCode The error code for conversion to its ptg value
725
     *
726
     * @return string The error code ptgErr
727
     */
728
    private function convertError($errorCode)
729
    {
730
        switch ($errorCode) {
731
            case '#NULL!':
732
                return pack('C', 0x00);
733
            case '#DIV/0!':
734
                return pack('C', 0x07);
735
            case '#VALUE!':
736
                return pack('C', 0x0F);
737
            case '#REF!':
738
                return pack('C', 0x17);
739
            case '#NAME?':
740
                return pack('C', 0x1D);
741
            case '#NUM!':
742
                return pack('C', 0x24);
743
            case '#N/A':
744
                return pack('C', 0x2A);
745
        }
746
747
        return pack('C', 0xFF);
748
    }
749
750 4
    private function convertDefinedName(string $name): string
751
    {
752 4
        if (strlen($name) > 255) {
753
            throw new WriterException('Defined Name is too long');
754
        }
755
756 4
        $nameReference = 1;
757 4
        foreach ($this->spreadsheet->getDefinedNames() as $definedName) {
758 4
            if ($name === $definedName->getName()) {
759 4
                break;
760
            }
761 3
            ++$nameReference;
762
        }
763
764 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...
765
766 4
        throw new WriterException('Cannot yet write formulae with defined names to Xls');
767
768
        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...
769
    }
770
771
    /**
772
     * Look up the REF index that corresponds to an external sheet name
773
     * (or range). If it doesn't exist yet add it to the workbook's references
774
     * array. It assumes all sheet names given must exist.
775
     *
776
     * @param string $ext_ref The name of the external reference
777
     *
778
     * @return mixed The reference index in packed() format on success
779
     */
780 6
    private function getRefIndex($ext_ref)
781
    {
782 6
        $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading  ' if any.
783 6
        $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any.
784 6
        $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with '
785
786
        // Check if there is a sheet range eg., Sheet1:Sheet2.
787 6
        if (preg_match('/:/', $ext_ref)) {
788
            [$sheet_name1, $sheet_name2] = explode(':', $ext_ref);
789
790
            $sheet1 = $this->getSheetIndex($sheet_name1);
791
            if ($sheet1 == -1) {
792
                throw new WriterException("Unknown sheet name $sheet_name1 in formula");
793
            }
794
            $sheet2 = $this->getSheetIndex($sheet_name2);
795
            if ($sheet2 == -1) {
796
                throw new WriterException("Unknown sheet name $sheet_name2 in formula");
797
            }
798
799
            // Reverse max and min sheet numbers if necessary
800
            if ($sheet1 > $sheet2) {
801
                [$sheet1, $sheet2] = [$sheet2, $sheet1];
802
            }
803
        } else { // Single sheet name only.
804 6
            $sheet1 = $this->getSheetIndex($ext_ref);
805 6
            if ($sheet1 == -1) {
806
                throw new WriterException("Unknown sheet name $ext_ref in formula");
807
            }
808 6
            $sheet2 = $sheet1;
809
        }
810
811
        // assume all references belong to this document
812 6
        $supbook_index = 0x00;
813 6
        $ref = pack('vvv', $supbook_index, $sheet1, $sheet2);
814 6
        $totalreferences = count($this->references);
815 6
        $index = -1;
816 6
        for ($i = 0; $i < $totalreferences; ++$i) {
817 6
            if ($ref == $this->references[$i]) {
818 6
                $index = $i;
819
820 6
                break;
821
            }
822
        }
823
        // if REF was not found add it to references array
824 6
        if ($index == -1) {
825
            $this->references[$totalreferences] = $ref;
826
            $index = $totalreferences;
827
        }
828
829 6
        return pack('v', $index);
830
    }
831
832
    /**
833
     * Look up the index that corresponds to an external sheet name. The hash of
834
     * sheet names is updated by the addworksheet() method of the
835
     * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class.
836
     *
837
     * @param string $sheet_name Sheet name
838
     *
839
     * @return int The sheet index, -1 if the sheet was not found
840
     */
841 6
    private function getSheetIndex($sheet_name)
842
    {
843 6
        if (!isset($this->externalSheets[$sheet_name])) {
844
            return -1;
845
        }
846
847 6
        return $this->externalSheets[$sheet_name];
848
    }
849
850
    /**
851
     * This method is used to update the array of sheet names. It is
852
     * called by the addWorksheet() method of the
853
     * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class.
854
     *
855
     * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet()
856
     *
857
     * @param string $name The name of the worksheet being added
858
     * @param int $index The index of the worksheet being added
859
     */
860 64
    public function setExtSheet($name, $index): void
861
    {
862 64
        $this->externalSheets[$name] = $index;
863 64
    }
864
865
    /**
866
     * pack() row and column into the required 3 or 4 byte format.
867
     *
868
     * @param string $cell The Excel cell reference to be packed
869
     *
870
     * @return array Array containing the row and column in packed() format
871
     */
872 21
    private function cellToPackedRowcol($cell)
873
    {
874 21
        $cell = strtoupper($cell);
875 21
        [$row, $col, $row_rel, $col_rel] = $this->cellToRowcol($cell);
876 21
        if ($col >= 256) {
877
            throw new WriterException("Column in: $cell greater than 255");
878
        }
879 21
        if ($row >= 65536) {
880
            throw new WriterException("Row in: $cell greater than 65536 ");
881
        }
882
883
        // Set the high bits to indicate if row or col are relative.
884 21
        $col |= $col_rel << 14;
885 21
        $col |= $row_rel << 15;
886 21
        $col = pack('v', $col);
887
888 21
        $row = pack('v', $row);
889
890 21
        return [$row, $col];
891
    }
892
893
    /**
894
     * pack() row range into the required 3 or 4 byte format.
895
     * Just using maximum col/rows, which is probably not the correct solution.
896
     *
897
     * @param string $range The Excel range to be packed
898
     *
899
     * @return array Array containing (row1,col1,row2,col2) in packed() format
900
     */
901
    private function rangeToPackedRange($range)
902
    {
903
        preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match);
904
        // return absolute rows if there is a $ in the ref
905
        $row1_rel = empty($match[1]) ? 1 : 0;
906
        $row1 = $match[2];
907
        $row2_rel = empty($match[3]) ? 1 : 0;
908
        $row2 = $match[4];
909
        // Convert 1-index to zero-index
910
        --$row1;
911
        --$row2;
912
        // Trick poor inocent Excel
913
        $col1 = 0;
914
        $col2 = 65535; // FIXME: maximum possible value for Excel 5 (change this!!!)
915
916
        // FIXME: this changes for BIFF8
917
        if (($row1 >= 65536) || ($row2 >= 65536)) {
918
            throw new WriterException("Row in: $range greater than 65536 ");
919
        }
920
921
        // Set the high bits to indicate if rows are relative.
922
        $col1 |= $row1_rel << 15;
923
        $col2 |= $row2_rel << 15;
924
        $col1 = pack('v', $col1);
925
        $col2 = pack('v', $col2);
926
927
        $row1 = pack('v', $row1);
928
        $row2 = pack('v', $row2);
929
930
        return [$row1, $col1, $row2, $col2];
931
    }
932
933
    /**
934
     * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero
935
     * indexed row and column number. Also returns two (0,1) values to indicate
936
     * whether the row or column are relative references.
937
     *
938
     * @param string $cell the Excel cell reference in A1 format
939
     *
940
     * @return array
941
     */
942 21
    private function cellToRowcol($cell)
943
    {
944 21
        preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match);
945
        // return absolute column if there is a $ in the ref
946 21
        $col_rel = empty($match[1]) ? 1 : 0;
947 21
        $col_ref = $match[2];
948 21
        $row_rel = empty($match[3]) ? 1 : 0;
949 21
        $row = $match[4];
950
951
        // Convert base26 column string to a number.
952 21
        $expn = strlen($col_ref) - 1;
953 21
        $col = 0;
954 21
        $col_ref_length = strlen($col_ref);
955 21
        for ($i = 0; $i < $col_ref_length; ++$i) {
956 21
            $col += (ord($col_ref[$i]) - 64) * 26 ** $expn;
957 21
            --$expn;
958
        }
959
960
        // Convert 1-index to zero-index
961 21
        --$row;
962 21
        --$col;
963
964 21
        return [$row, $col, $row_rel, $col_rel];
965
    }
966
967
    /**
968
     * Advance to the next valid token.
969
     */
970 23
    private function advance()
971
    {
972 23
        $token = '';
973 23
        $i = $this->currentCharacter;
974 23
        $formula_length = strlen($this->formula);
975
        // eat up white spaces
976 23
        if ($i < $formula_length) {
977 23
            while ($this->formula[$i] == ' ') {
978 2
                ++$i;
979
            }
980
981 23
            if ($i < ($formula_length - 1)) {
982 23
                $this->lookAhead = $this->formula[$i + 1];
983
            }
984 23
            $token = '';
985
        }
986
987 23
        while ($i < $formula_length) {
988 23
            $token .= $this->formula[$i];
989
990 23
            if ($i < ($formula_length - 1)) {
991 23
                $this->lookAhead = $this->formula[$i + 1];
992
            } else {
993 23
                $this->lookAhead = '';
994
            }
995
996 23
            if ($this->match($token) != '') {
997 23
                $this->currentCharacter = $i + 1;
998 23
                $this->currentToken = $token;
999
1000 23
                return 1;
1001
            }
1002
1003 23
            if ($i < ($formula_length - 2)) {
1004 23
                $this->lookAhead = $this->formula[$i + 2];
1005
            } else { // if we run out of characters lookAhead becomes empty
1006 16
                $this->lookAhead = '';
1007
            }
1008 23
            ++$i;
1009
        }
1010
        //die("Lexical error ".$this->currentCharacter);
1011 23
    }
1012
1013
    /**
1014
     * Checks if it's a valid token.
1015
     *
1016
     * @param mixed $token the token to check
1017
     *
1018
     * @return mixed The checked token or false on failure
1019
     */
1020 23
    private function match($token)
1021
    {
1022
        switch ($token) {
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 23
            case '%':
1038 22
                return $token;
1039
1040
                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...
1041 23
            case '>':
1042
                if ($this->lookAhead === '=') { // it's a GE token
1043
                    break;
1044
                }
1045
1046
                return $token;
1047
1048
                break;
1049 23
            case '<':
1050
                // it's a LE or a NE token
1051 5
                if (($this->lookAhead === '=') || ($this->lookAhead === '>')) {
1052 5
                    break;
1053
                }
1054
1055
                return $token;
1056
1057
                break;
1058
            default:
1059
                // if it's a reference A1 or $A$1 or $A1 or A$1
1060 23
                if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) {
1061 13
                    return $token;
1062 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 !== '.')) {
1063
                    // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
1064
                    return $token;
1065 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 !== '.')) {
1066
                    // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
1067 4
                    return $token;
1068 23
                } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) {
1069
                    // if it's a range A1:A2 or $A$1:$A$2
1070 17
                    return $token;
1071 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)) {
1072
                    // 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
1073
                    return $token;
1074 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)) {
1075
                    // 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
1076 4
                    return $token;
1077 23
                } elseif (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) {
1078
                    // If it's a number (check that it's not a sheet name or range)
1079 12
                    return $token;
1080 23
                } elseif (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) {
1081
                    // If it's a string (of maximum 255 characters)
1082 10
                    return $token;
1083 23
                } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') {
1084
                    // If it's an error code
1085
                    return $token;
1086 23
                } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) {
1087
                    // if it's a function call
1088 19
                    return $token;
1089 23
                } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) {
1090 4
                    return $token;
1091 23
                } elseif (substr($token, -1) === ')') {
1092
                    //    It's an argument of some description (e.g. a named range),
1093
                    //        precise nature yet to be determined
1094 1
                    return $token;
1095
                }
1096
1097 23
                return '';
1098
        }
1099 5
    }
1100
1101
    /**
1102
     * The parsing method. It parses a formula.
1103
     *
1104
     * @param string $formula the formula to parse, without the initial equal
1105
     *                        sign (=)
1106
     *
1107
     * @return mixed true on success
1108
     */
1109 23
    public function parse($formula)
1110
    {
1111 23
        $this->currentCharacter = 0;
1112 23
        $this->formula = (string) $formula;
1113 23
        $this->lookAhead = $formula[1] ?? '';
1114 23
        $this->advance();
1115 23
        $this->parseTree = $this->condition();
1116
1117 23
        return true;
1118
    }
1119
1120
    /**
1121
     * It parses a condition. It assumes the following rule:
1122
     * Cond -> Expr [(">" | "<") Expr].
1123
     *
1124
     * @return mixed The parsed ptg'd tree on success
1125
     */
1126 23
    private function condition()
1127
    {
1128 23
        $result = $this->expression();
1129 23
        if ($this->currentToken == '<') {
1130
            $this->advance();
1131
            $result2 = $this->expression();
1132
            $result = $this->createTree('ptgLT', $result, $result2);
1133 23
        } elseif ($this->currentToken == '>') {
1134
            $this->advance();
1135
            $result2 = $this->expression();
1136
            $result = $this->createTree('ptgGT', $result, $result2);
1137 23
        } elseif ($this->currentToken == '<=') {
1138
            $this->advance();
1139
            $result2 = $this->expression();
1140
            $result = $this->createTree('ptgLE', $result, $result2);
1141 23
        } elseif ($this->currentToken == '>=') {
1142
            $this->advance();
1143
            $result2 = $this->expression();
1144
            $result = $this->createTree('ptgGE', $result, $result2);
1145 23
        } elseif ($this->currentToken == '=') {
1146 1
            $this->advance();
1147 1
            $result2 = $this->expression();
1148 1
            $result = $this->createTree('ptgEQ', $result, $result2);
1149 23
        } elseif ($this->currentToken == '<>') {
1150 5
            $this->advance();
1151 5
            $result2 = $this->expression();
1152 5
            $result = $this->createTree('ptgNE', $result, $result2);
1153 23
        } elseif ($this->currentToken == '&') {
1154 6
            $this->advance();
1155 6
            $result2 = $this->expression();
1156 6
            $result = $this->createTree('ptgConcat', $result, $result2);
1157
        }
1158
1159 23
        return $result;
1160
    }
1161
1162
    /**
1163
     * It parses a expression. It assumes the following rule:
1164
     * Expr -> Term [("+" | "-") Term]
1165
     *      -> "string"
1166
     *      -> "-" Term : Negative value
1167
     *      -> "+" Term : Positive value
1168
     *      -> Error code.
1169
     *
1170
     * @return mixed The parsed ptg'd tree on success
1171
     */
1172 23
    private function expression()
1173
    {
1174
        // If it's a string return a string node
1175 23
        if (preg_match('/"([^"]|""){0,255}"/', $this->currentToken)) {
1176 10
            $tmp = str_replace('""', '"', $this->currentToken);
1177 10
            if (($tmp == '"') || ($tmp == '')) {
1178
                //    Trap for "" that has been used for an empty string
1179 5
                $tmp = '""';
1180
            }
1181 10
            $result = $this->createTree($tmp, '', '');
1182 10
            $this->advance();
1183
1184 10
            return $result;
1185
        // If it's an error code
1186 23
        } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) || $this->currentToken == '#N/A') {
1187
            $result = $this->createTree($this->currentToken, 'ptgErr', '');
1188
            $this->advance();
1189
1190
            return $result;
1191
        // If it's a negative value
1192 23
        } elseif ($this->currentToken == '-') {
1193
            // catch "-" Term
1194
            $this->advance();
1195
            $result2 = $this->expression();
1196
1197
            return $this->createTree('ptgUminus', $result2, '');
1198
        // If it's a positive value
1199 23
        } elseif ($this->currentToken == '+') {
1200
            // catch "+" Term
1201
            $this->advance();
1202
            $result2 = $this->expression();
1203
1204
            return $this->createTree('ptgUplus', $result2, '');
1205
        }
1206 23
        $result = $this->term();
1207
        while (
1208 23
            ($this->currentToken == '+') ||
1209 23
            ($this->currentToken == '-') ||
1210 23
            ($this->currentToken == '^')
1211
        ) {
1212 10
            if ($this->currentToken == '+') {
1213 10
                $this->advance();
1214 10
                $result2 = $this->term();
1215 10
                $result = $this->createTree('ptgAdd', $result, $result2);
1216 3
            } elseif ($this->currentToken == '-') {
1217 3
                $this->advance();
1218 3
                $result2 = $this->term();
1219 3
                $result = $this->createTree('ptgSub', $result, $result2);
1220
            } else {
1221
                $this->advance();
1222
                $result2 = $this->term();
1223
                $result = $this->createTree('ptgPower', $result, $result2);
1224
            }
1225
        }
1226
1227 23
        return $result;
1228
    }
1229
1230
    /**
1231
     * This function just introduces a ptgParen element in the tree, so that Excel
1232
     * doesn't get confused when working with a parenthesized formula afterwards.
1233
     *
1234
     * @see fact()
1235
     *
1236
     * @return array The parsed ptg'd tree
1237
     */
1238 1
    private function parenthesizedExpression()
1239
    {
1240 1
        return $this->createTree('ptgParen', $this->expression(), '');
1241
    }
1242
1243
    /**
1244
     * It parses a term. It assumes the following rule:
1245
     * Term -> Fact [("*" | "/") Fact].
1246
     *
1247
     * @return mixed The parsed ptg'd tree on success
1248
     */
1249 23
    private function term()
1250
    {
1251 23
        $result = $this->fact();
1252
        while (
1253 23
            ($this->currentToken == '*') ||
1254 23
            ($this->currentToken == '/')
1255
        ) {
1256 12
            if ($this->currentToken == '*') {
1257 12
                $this->advance();
1258 12
                $result2 = $this->fact();
1259 12
                $result = $this->createTree('ptgMul', $result, $result2);
1260
            } else {
1261 2
                $this->advance();
1262 2
                $result2 = $this->fact();
1263 2
                $result = $this->createTree('ptgDiv', $result, $result2);
1264
            }
1265
        }
1266
1267 23
        return $result;
1268
    }
1269
1270
    /**
1271
     * It parses a factor. It assumes the following rule:
1272
     * Fact -> ( Expr )
1273
     *       | CellRef
1274
     *       | CellRange
1275
     *       | Number
1276
     *       | Function.
1277
     *
1278
     * @return mixed The parsed ptg'd tree on success
1279
     */
1280 23
    private function fact()
1281
    {
1282 23
        if ($this->currentToken === '(') {
1283 1
            $this->advance(); // eat the "("
1284 1
            $result = $this->parenthesizedExpression();
1285 1
            if ($this->currentToken !== ')') {
1286
                throw new WriterException("')' token expected.");
1287
            }
1288 1
            $this->advance(); // eat the ")"
1289
1290 1
            return $result;
1291
        }
1292
        // if it's a reference
1293 23
        if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) {
1294 13
            $result = $this->createTree($this->currentToken, '', '');
1295 13
            $this->advance();
1296
1297 13
            return $result;
1298 22
        } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) {
1299
            // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
1300
            $result = $this->createTree($this->currentToken, '', '');
1301
            $this->advance();
1302
1303
            return $result;
1304 22
        } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) {
1305
            // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
1306 4
            $result = $this->createTree($this->currentToken, '', '');
1307 4
            $this->advance();
1308
1309 4
            return $result;
1310
        } elseif (
1311 22
            preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) ||
1312 22
            preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken)
1313
        ) {
1314
            // if it's a range A1:B2 or $A$1:$B$2
1315
            // must be an error?
1316 17
            $result = $this->createTree($this->currentToken, '', '');
1317 17
            $this->advance();
1318
1319 17
            return $result;
1320 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)) {
1321
            // 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)
1322
            // must be an error?
1323
            $result = $this->createTree($this->currentToken, '', '');
1324
            $this->advance();
1325
1326
            return $result;
1327 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)) {
1328
            // 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)
1329
            // must be an error?
1330 4
            $result = $this->createTree($this->currentToken, '', '');
1331 4
            $this->advance();
1332
1333 4
            return $result;
1334 22
        } elseif (is_numeric($this->currentToken)) {
1335
            // If it's a number or a percent
1336 12
            if ($this->lookAhead === '%') {
1337
                $result = $this->createTree('ptgPercent', $this->currentToken, '');
1338
                $this->advance(); // Skip the percentage operator once we've pre-built that tree
1339
            } else {
1340 12
                $result = $this->createTree($this->currentToken, '', '');
1341
            }
1342 12
            $this->advance();
1343
1344 12
            return $result;
1345 21
        } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) {
1346
            // if it's a function call
1347 19
            return $this->func();
1348 6
        } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) {
1349 4
            $result = $this->createTree('ptgName', $this->currentToken, '');
1350 4
            $this->advance();
1351
1352 4
            return $result;
1353
        }
1354
1355 2
        throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter);
1356
    }
1357
1358
    /**
1359
     * It parses a function call. It assumes the following rule:
1360
     * Func -> ( Expr [,Expr]* ).
1361
     *
1362
     * @return mixed The parsed ptg'd tree on success
1363
     */
1364 19
    private function func()
1365
    {
1366 19
        $num_args = 0; // number of arguments received
1367 19
        $function = strtoupper($this->currentToken);
1368 19
        $result = ''; // initialize result
1369 19
        $this->advance();
1370 19
        $this->advance(); // eat the "("
1371 19
        while ($this->currentToken !== ')') {
1372 19
            if ($num_args > 0) {
1373 8
                if ($this->currentToken === ',' || $this->currentToken === ';') {
1374 8
                    $this->advance(); // eat the "," or ";"
1375
                } else {
1376
                    throw new WriterException("Syntax error: comma expected in function $function, arg #{$num_args}");
1377
                }
1378 8
                $result2 = $this->condition();
1379 8
                $result = $this->createTree('arg', $result, $result2);
1380
            } else { // first argument
1381 19
                $result2 = $this->condition();
1382 19
                $result = $this->createTree('arg', '', $result2);
1383
            }
1384 19
            ++$num_args;
1385
        }
1386 19
        if (!isset($this->functions[$function])) {
1387 2
            throw new WriterException("Function $function() doesn't exist");
1388
        }
1389 18
        $args = $this->functions[$function][1];
1390
        // If fixed number of args eg. TIME($i, $j, $k). Check that the number of args is valid.
1391 18
        if (($args >= 0) && ($args != $num_args)) {
1392
            throw new WriterException("Incorrect number of arguments in function $function() ");
1393
        }
1394
1395 18
        $result = $this->createTree($function, $result, $num_args);
1396 18
        $this->advance(); // eat the ")"
1397
1398 18
        return $result;
1399
    }
1400
1401
    /**
1402
     * Creates a tree. In fact an array which may have one or two arrays (sub-trees)
1403
     * as elements.
1404
     *
1405
     * @param mixed $value the value of this node
1406
     * @param mixed $left the left array (sub-tree) or a final node
1407
     * @param mixed $right the right array (sub-tree) or a final node
1408
     *
1409
     * @return array A tree
1410
     */
1411 23
    private function createTree($value, $left, $right)
1412
    {
1413 23
        return ['value' => $value, 'left' => $left, 'right' => $right];
1414
    }
1415
1416
    /**
1417
     * Builds a string containing the tree in reverse polish notation (What you
1418
     * would use in a HP calculator stack).
1419
     * The following tree:.
1420
     *
1421
     *    +
1422
     *   / \
1423
     *  2   3
1424
     *
1425
     * produces: "23+"
1426
     *
1427
     * The following tree:
1428
     *
1429
     *    +
1430
     *   / \
1431
     *  3   *
1432
     *     / \
1433
     *    6   A1
1434
     *
1435
     * produces: "36A1*+"
1436
     *
1437
     * In fact all operands, functions, references, etc... are written as ptg's
1438
     *
1439
     * @param array $tree the optional tree to convert
1440
     *
1441
     * @return string The tree in reverse polish notation
1442
     */
1443 23
    public function toReversePolish($tree = [])
1444
    {
1445 23
        $polish = ''; // the string we are going to return
1446 23
        if (empty($tree)) { // If it's the first call use parseTree
1447 23
            $tree = $this->parseTree;
1448
        }
1449
1450 23
        if (is_array($tree['left'])) {
1451 21
            $converted_tree = $this->toReversePolish($tree['left']);
1452 21
            $polish .= $converted_tree;
1453 23
        } elseif ($tree['left'] != '') { // It's a final node
1454 4
            $converted_tree = $this->convert($tree['left']);
1455
            $polish .= $converted_tree;
1456
        }
1457 23
        if (is_array($tree['right'])) {
1458 21
            $converted_tree = $this->toReversePolish($tree['right']);
1459 20
            $polish .= $converted_tree;
1460 23
        } elseif ($tree['right'] != '') { // It's a final node
1461 18
            $converted_tree = $this->convert($tree['right']);
1462 18
            $polish .= $converted_tree;
1463
        }
1464
        // if it's a function convert it here (so we can set it's arguments)
1465
        if (
1466 23
            preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/", $tree['value']) &&
1467 23
            !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value']) &&
1468 23
            !preg_match('/^[A-Ia-i]?[A-Za-z](\\d+)\\.\\.[A-Ia-i]?[A-Za-z](\\d+)$/', $tree['value']) &&
1469 23
            !is_numeric($tree['value']) &&
1470 23
            !isset($this->ptg[$tree['value']])
1471
        ) {
1472
            // left subtree for a function is always an array.
1473 18
            if ($tree['left'] != '') {
1474 18
                $left_tree = $this->toReversePolish($tree['left']);
1475
            } else {
1476 4
                $left_tree = '';
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