Passed
Push — master ( 75dfcb...5ec0e3 )
by Adrien
27:56
created

Parser::match()   D

Complexity

Conditions 53
Paths 31

Size

Total Lines 76
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 61.1968

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 53
eloc 52
c 1
b 0
f 0
nc 31
nop 1
dl 0
loc 76
ccs 42
cts 49
cp 0.8571
crap 61.1968
rs 4.1666

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