Test Failed
Push — develop ( 90366f...812a46 )
by Adrien
28:16
created

Parser::convertRef2d()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

565
        if (strlen(/** @scrutinizer ignore-type */ $string) > 255) {
Loading history...
566
            throw new WriterException('String is too long');
567
        }
568
569 9
        return pack('C', $this->ptg['ptgStr']) . StringHelper::UTF8toBIFF8UnicodeShort($string);
1 ignored issue
show
Bug introduced by
It seems like $string can also be of type false; however, parameter $value of PhpOffice\PhpSpreadsheet...F8toBIFF8UnicodeShort() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

569
        return pack('C', $this->ptg['ptgStr']) . StringHelper::UTF8toBIFF8UnicodeShort(/** @scrutinizer ignore-type */ $string);
Loading history...
570
    }
571
572
    /**
573
     * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of
574
     * args that it takes.
575
     *
576
     * @param string $token the name of the function for convertion to ptg value
577
     * @param int $num_args the number of arguments the function receives
578
     *
579
     * @return string The packed ptg for the function
580
     */
581 15
    private function convertFunction($token, $num_args)
582
    {
583 15
        $args = $this->functions[$token][1];
584
585
        // Fixed number of args eg. TIME($i, $j, $k).
586 15
        if ($args >= 0) {
587 4
            return pack('Cv', $this->ptg['ptgFuncV'], $this->functions[$token][0]);
588
        }
589
        // Variable number of args eg. SUM($i, $j, $k, ..).
590 15
        if ($args == -1) {
591 15
            return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]);
592
        }
593
    }
594
595
    /**
596
     * Convert an Excel range such as A1:D4 to a ptgRefV.
597
     *
598
     * @param string $range An Excel range in the A1:A2
599
     * @param int $class
600
     */
601 15
    private function convertRange2d($range, $class = 0)
602
    {
603
        // TODO: possible class value 0,1,2 check Formula.pm
604
        // Split the range into 2 cell refs
605 15
        if (preg_match('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) {
606 15
            list($cell1, $cell2) = explode(':', $range);
607
        } else {
608
            // TODO: use real error codes
609
            throw new WriterException('Unknown range separator');
610
        }
611
612
        // Convert the cell references
613 15
        list($row1, $col1) = $this->cellToPackedRowcol($cell1);
614 15
        list($row2, $col2) = $this->cellToPackedRowcol($cell2);
615
616
        // The ptg value depends on the class of the ptg.
617 15
        if ($class == 0) {
618 15
            $ptgArea = pack('C', $this->ptg['ptgArea']);
619
        } elseif ($class == 1) {
620
            $ptgArea = pack('C', $this->ptg['ptgAreaV']);
621
        } elseif ($class == 2) {
622
            $ptgArea = pack('C', $this->ptg['ptgAreaA']);
623
        } else {
624
            // TODO: use real error codes
625
            throw new WriterException("Unknown class $class");
626
        }
627
628 15
        return $ptgArea . $row1 . $row2 . $col1 . $col2;
629
    }
630
631
    /**
632
     * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to
633
     * a ptgArea3d.
634
     *
635
     * @param string $token an Excel range in the Sheet1!A1:A2 format
636
     *
637
     * @return mixed the packed ptgArea3d token on success
638
     */
639 3
    private function convertRange3d($token)
640
    {
641
        // Split the ref at the ! symbol
642 3
        list($ext_ref, $range) = explode('!', $token);
643
644
        // Convert the external reference part (different for BIFF8)
645 3
        $ext_ref = $this->getRefIndex($ext_ref);
646
647
        // Split the range into 2 cell refs
648 3
        list($cell1, $cell2) = explode(':', $range);
649
650
        // Convert the cell references
651 3
        if (preg_match("/^(\\$)?[A-Ia-i]?[A-Za-z](\\$)?(\d+)$/", $cell1)) {
652 3
            list($row1, $col1) = $this->cellToPackedRowcol($cell1);
653 3
            list($row2, $col2) = $this->cellToPackedRowcol($cell2);
654
        } else { // It's a rows range (like 26:27)
655
            list($row1, $col1, $row2, $col2) = $this->rangeToPackedRange($cell1 . ':' . $cell2);
656
        }
657
658
        // The ptg value depends on the class of the ptg.
659 3
        $ptgArea = pack('C', $this->ptg['ptgArea3d']);
660
661 3
        return $ptgArea . $ext_ref . $row1 . $row2 . $col1 . $col2;
662
    }
663
664
    /**
665
     * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV.
666
     *
667
     * @param string $cell An Excel cell reference
668
     *
669
     * @return string The cell in packed() format with the corresponding ptg
670
     */
671 12
    private function convertRef2d($cell)
672
    {
673
        // Convert the cell reference
674 12
        $cell_array = $this->cellToPackedRowcol($cell);
675 12
        list($row, $col) = $cell_array;
676
677
        // The ptg value depends on the class of the ptg.
678 12
        $ptgRef = pack('C', $this->ptg['ptgRefA']);
679
680 12
        return $ptgRef . $row . $col;
681
    }
682
683
    /**
684
     * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a
685
     * ptgRef3d.
686
     *
687
     * @param string $cell An Excel cell reference
688
     *
689
     * @return mixed the packed ptgRef3d token on success
690
     */
691 1
    private function convertRef3d($cell)
692
    {
693
        // Split the ref at the ! symbol
694 1
        list($ext_ref, $cell) = explode('!', $cell);
695
696
        // Convert the external reference part (different for BIFF8)
697 1
        $ext_ref = $this->getRefIndex($ext_ref);
698
699
        // Convert the cell reference part
700 1
        list($row, $col) = $this->cellToPackedRowcol($cell);
701
702
        // The ptg value depends on the class of the ptg.
703 1
        $ptgRef = pack('C', $this->ptg['ptgRef3dA']);
704
705 1
        return $ptgRef . $ext_ref . $row . $col;
706
    }
707
708
    /**
709
     * Convert an error code to a ptgErr.
710
     *
711
     * @param string $errorCode The error code for conversion to its ptg value
712
     *
713
     * @return string The error code ptgErr
714
     */
715
    private function convertError($errorCode)
716
    {
717
        switch ($errorCode) {
718
            case '#NULL!':
719
                return pack('C', 0x00);
720
            case '#DIV/0!':
721
                return pack('C', 0x07);
722
            case '#VALUE!':
723
                return pack('C', 0x0F);
724
            case '#REF!':
725
                return pack('C', 0x17);
726
            case '#NAME?':
727
                return pack('C', 0x1D);
728
            case '#NUM!':
729
                return pack('C', 0x24);
730
            case '#N/A':
731
                return pack('C', 0x2A);
732
        }
733
734
        return pack('C', 0xFF);
735
    }
736
737
    /**
738
     * Look up the REF index that corresponds to an external sheet name
739
     * (or range). If it doesn't exist yet add it to the workbook's references
740
     * array. It assumes all sheet names given must exist.
741
     *
742
     * @param string $ext_ref The name of the external reference
743
     *
744
     * @return mixed The reference index in packed() format on success
745
     */
746 4
    private function getRefIndex($ext_ref)
747
    {
748 4
        $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading  ' if any.
749 4
        $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any.
750 4
        $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with '
751
752
        // Check if there is a sheet range eg., Sheet1:Sheet2.
753 4
        if (preg_match('/:/', $ext_ref)) {
754
            list($sheet_name1, $sheet_name2) = explode(':', $ext_ref);
755
756
            $sheet1 = $this->getSheetIndex($sheet_name1);
757
            if ($sheet1 == -1) {
758
                throw new WriterException("Unknown sheet name $sheet_name1 in formula");
759
            }
760
            $sheet2 = $this->getSheetIndex($sheet_name2);
761
            if ($sheet2 == -1) {
762
                throw new WriterException("Unknown sheet name $sheet_name2 in formula");
763
            }
764
765
            // Reverse max and min sheet numbers if necessary
766
            if ($sheet1 > $sheet2) {
767
                list($sheet1, $sheet2) = [$sheet2, $sheet1];
768
            }
769
        } else { // Single sheet name only.
770 4
            $sheet1 = $this->getSheetIndex($ext_ref);
771 4
            if ($sheet1 == -1) {
772
                throw new WriterException("Unknown sheet name $ext_ref in formula");
773
            }
774 4
            $sheet2 = $sheet1;
775
        }
776
777
        // assume all references belong to this document
778 4
        $supbook_index = 0x00;
779 4
        $ref = pack('vvv', $supbook_index, $sheet1, $sheet2);
780 4
        $totalreferences = count($this->references);
781 4
        $index = -1;
782 4
        for ($i = 0; $i < $totalreferences; ++$i) {
783 4
            if ($ref == $this->references[$i]) {
784 4
                $index = $i;
785
786 4
                break;
787
            }
788
        }
789
        // if REF was not found add it to references array
790 4
        if ($index == -1) {
791
            $this->references[$totalreferences] = $ref;
792
            $index = $totalreferences;
793
        }
794
795 4
        return pack('v', $index);
796
    }
797
798
    /**
799
     * Look up the index that corresponds to an external sheet name. The hash of
800
     * sheet names is updated by the addworksheet() method of the
801
     * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class.
802
     *
803
     * @param string $sheet_name Sheet name
804
     *
805
     * @return int The sheet index, -1 if the sheet was not found
806
     */
807 4
    private function getSheetIndex($sheet_name)
808
    {
809 4
        if (!isset($this->externalSheets[$sheet_name])) {
810
            return -1;
811
        }
812
813 4
        return $this->externalSheets[$sheet_name];
814
    }
815
816
    /**
817
     * This method is used to update the array of sheet names. It is
818
     * called by the addWorksheet() method of the
819
     * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class.
820
     *
821
     * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet()
822
     *
823
     * @param string $name The name of the worksheet being added
824
     * @param int $index The index of the worksheet being added
825
     */
826 48
    public function setExtSheet($name, $index)
827
    {
828 48
        $this->externalSheets[$name] = $index;
829 48
    }
830
831
    /**
832
     * pack() row and column into the required 3 or 4 byte format.
833
     *
834
     * @param string $cell The Excel cell reference to be packed
835
     *
836
     * @return array Array containing the row and column in packed() format
837
     */
838 17
    private function cellToPackedRowcol($cell)
839
    {
840 17
        $cell = strtoupper($cell);
841 17
        list($row, $col, $row_rel, $col_rel) = $this->cellToRowcol($cell);
842 17
        if ($col >= 256) {
843
            throw new WriterException("Column in: $cell greater than 255");
844
        }
845 17
        if ($row >= 65536) {
846
            throw new WriterException("Row in: $cell greater than 65536 ");
847
        }
848
849
        // Set the high bits to indicate if row or col are relative.
850 17
        $col |= $col_rel << 14;
851 17
        $col |= $row_rel << 15;
852 17
        $col = pack('v', $col);
853
854 17
        $row = pack('v', $row);
855
856 17
        return [$row, $col];
857
    }
858
859
    /**
860
     * pack() row range into the required 3 or 4 byte format.
861
     * Just using maximum col/rows, which is probably not the correct solution.
862
     *
863
     * @param string $range The Excel range to be packed
864
     *
865
     * @return array Array containing (row1,col1,row2,col2) in packed() format
866
     */
867
    private function rangeToPackedRange($range)
868
    {
869
        preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match);
870
        // return absolute rows if there is a $ in the ref
871
        $row1_rel = empty($match[1]) ? 1 : 0;
872
        $row1 = $match[2];
873
        $row2_rel = empty($match[3]) ? 1 : 0;
874
        $row2 = $match[4];
875
        // Convert 1-index to zero-index
876
        --$row1;
877
        --$row2;
878
        // Trick poor inocent Excel
879
        $col1 = 0;
880
        $col2 = 65535; // FIXME: maximum possible value for Excel 5 (change this!!!)
881
882
        // FIXME: this changes for BIFF8
883
        if (($row1 >= 65536) or ($row2 >= 65536)) {
884
            throw new WriterException("Row in: $range greater than 65536 ");
885
        }
886
887
        // Set the high bits to indicate if rows are relative.
888
        $col1 |= $row1_rel << 15;
889
        $col2 |= $row2_rel << 15;
890
        $col1 = pack('v', $col1);
891
        $col2 = pack('v', $col2);
892
893
        $row1 = pack('v', $row1);
894
        $row2 = pack('v', $row2);
895
896
        return [$row1, $col1, $row2, $col2];
897
    }
898
899
    /**
900
     * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero
901
     * indexed row and column number. Also returns two (0,1) values to indicate
902
     * whether the row or column are relative references.
903
     *
904
     * @param string $cell the Excel cell reference in A1 format
905
     *
906
     * @return array
907
     */
908 17
    private function cellToRowcol($cell)
909
    {
910 17
        preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match);
911
        // return absolute column if there is a $ in the ref
912 17
        $col_rel = empty($match[1]) ? 1 : 0;
913 17
        $col_ref = $match[2];
914 17
        $row_rel = empty($match[3]) ? 1 : 0;
915 17
        $row = $match[4];
916
917
        // Convert base26 column string to a number.
918 17
        $expn = strlen($col_ref) - 1;
919 17
        $col = 0;
920 17
        $col_ref_length = strlen($col_ref);
921 17
        for ($i = 0; $i < $col_ref_length; ++$i) {
922 17
            $col += (ord($col_ref[$i]) - 64) * pow(26, $expn);
923 17
            --$expn;
924
        }
925
926
        // Convert 1-index to zero-index
927 17
        --$row;
928 17
        --$col;
929
930 17
        return [$row, $col, $row_rel, $col_rel];
931
    }
932
933
    /**
934
     * Advance to the next valid token.
935
     */
936 18
    private function advance()
937
    {
938 18
        $i = $this->currentCharacter;
939 18
        $formula_length = strlen($this->formula);
940
        // eat up white spaces
941 18
        if ($i < $formula_length) {
942 18
            while ($this->formula[$i] == ' ') {
943 2
                ++$i;
944
            }
945
946 18
            if ($i < ($formula_length - 1)) {
947 18
                $this->lookAhead = $this->formula[$i + 1];
948
            }
949 18
            $token = '';
950
        }
951
952 18
        while ($i < $formula_length) {
953 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...
954
955 18 View Code Duplication
            if ($i < ($formula_length - 1)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
956 18
                $this->lookAhead = $this->formula[$i + 1];
957
            } else {
958 18
                $this->lookAhead = '';
959
            }
960
961 18
            if ($this->match($token) != '') {
962 18
                $this->currentCharacter = $i + 1;
963 18
                $this->currentToken = $token;
964
965 18
                return 1;
966
            }
967
968 18 View Code Duplication
            if ($i < ($formula_length - 2)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
969 18
                $this->lookAhead = $this->formula[$i + 2];
970
            } else { // if we run out of characters lookAhead becomes empty
971 14
                $this->lookAhead = '';
972
            }
973 18
            ++$i;
974
        }
975
        //die("Lexical error ".$this->currentCharacter);
976 18
    }
977
978
    /**
979
     * Checks if it's a valid token.
980
     *
981
     * @param mixed $token the token to check
982
     *
983
     * @return mixed The checked token or false on failure
984
     */
985 18
    private function match($token)
986
    {
987
        switch ($token) {
988 18
            case '+':
989 18
            case '-':
990 18
            case '*':
991 18
            case '/':
992 18
            case '(':
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 17
                return $token;
1004
                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...
1005 18
            case '>':
1006
                if ($this->lookAhead == '=') { // it's a GE token
1007
                    break;
1008
                }
1009
1010
                return $token;
1011
                break;
1012 18
            case '<':
1013
                // it's a LE or a NE token
1014 5
                if (($this->lookAhead == '=') or ($this->lookAhead == '>')) {
1015 5
                    break;
1016
                }
1017
1018
                return $token;
1019
                break;
1020
            default:
1021
                // if it's a reference A1 or $A$1 or $A1 or A$1
1022 18
                if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?[0-9]+$/', $token) and !preg_match('/[0-9]/', $this->lookAhead) and ($this->lookAhead != ':') and ($this->lookAhead != '.') and ($this->lookAhead != '!')) {
1023 12
                    return $token;
1024 18
                } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . "(\:" . self::REGEX_SHEET_TITLE_UNQUOTED . ")?\!\\$?[A-Ia-i]?[A-Za-z]\\$?[0-9]+$/u", $token) and !preg_match('/[0-9]/', $this->lookAhead) and ($this->lookAhead != ':') and ($this->lookAhead != '.')) {
1025
                    // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
1026
                    return $token;
1027 18
                } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . "(\:" . self::REGEX_SHEET_TITLE_QUOTED . ")?'\!\\$?[A-Ia-i]?[A-Za-z]\\$?[0-9]+$/u", $token) and !preg_match('/[0-9]/', $this->lookAhead) and ($this->lookAhead != ':') and ($this->lookAhead != '.')) {
1028
                    // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
1029 1
                    return $token;
1030 18
                } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+:(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/', $token) && !preg_match('/[0-9]/', $this->lookAhead)) {
1031
                    // if it's a range A1:A2 or $A$1:$A$2
1032 15
                    return $token;
1033 18
                } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . "(\:" . self::REGEX_SHEET_TITLE_UNQUOTED . ")?\!\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+:\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+$/u", $token) and !preg_match('/[0-9]/', $this->lookAhead)) {
1034
                    // 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
1035
                    return $token;
1036 18
                } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . "(\:" . self::REGEX_SHEET_TITLE_QUOTED . ")?'\!\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+:\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+$/u", $token) and !preg_match('/[0-9]/', $this->lookAhead)) {
1037
                    // 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
1038 3
                    return $token;
1039 18
                } elseif (is_numeric($token) and (!is_numeric($token . $this->lookAhead) or ($this->lookAhead == '')) and ($this->lookAhead != '!') and ($this->lookAhead != ':')) {
1040
                    // If it's a number (check that it's not a sheet name or range)
1041 10
                    return $token;
1042 18
                } elseif (preg_match('/"([^"]|""){0,255}"/', $token) and $this->lookAhead != '"' and (substr_count($token, '"') % 2 == 0)) {
1043
                    // If it's a string (of maximum 255 characters)
1044 9
                    return $token;
1045 18
                } elseif (preg_match("/^#[A-Z0\/]{3,5}[!?]{1}$/", $token) or $token == '#N/A') {
1046
                    // If it's an error code
1047
                    return $token;
1048 18
                } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\.]+$/i", $token) and ($this->lookAhead == '(')) {
1049
                    // if it's a function call
1050 15
                    return $token;
1051 18
                } elseif (substr($token, -1) == ')') {
1052
                    //    It's an argument of some description (e.g. a named range),
1053
                    //        precise nature yet to be determined
1054 3
                    return $token;
1055
                }
1056
1057 18
                return '';
1058
        }
1059 5
    }
1060
1061
    /**
1062
     * The parsing method. It parses a formula.
1063
     *
1064
     * @param string $formula the formula to parse, without the initial equal
1065
     *                        sign (=)
1066
     *
1067
     * @return mixed true on success
1068
     */
1069 18
    public function parse($formula)
1070
    {
1071 18
        $this->currentCharacter = 0;
1072 18
        $this->formula = $formula;
1073 18
        $this->lookAhead = isset($formula[1]) ? $formula[1]
1074 1
        : '';
1075 18
        $this->advance();
1076 18
        $this->parseTree = $this->condition();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->condition() of type array is incompatible with the declared type string of property $parseTree.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1077
1078 18
        return true;
1079
    }
1080
1081
    /**
1082
     * It parses a condition. It assumes the following rule:
1083
     * Cond -> Expr [(">" | "<") Expr].
1084
     *
1085
     * @return mixed The parsed ptg'd tree on success
1086
     */
1087 18
    private function condition()
1088
    {
1089 18
        $result = $this->expression();
1090 18
        if ($this->currentToken == '<') {
1091
            $this->advance();
1092
            $result2 = $this->expression();
1093
            $result = $this->createTree('ptgLT', $result, $result2);
1094 18 View Code Duplication
        } elseif ($this->currentToken == '>') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1095
            $this->advance();
1096
            $result2 = $this->expression();
1097
            $result = $this->createTree('ptgGT', $result, $result2);
1098 18
        } elseif ($this->currentToken == '<=') {
1099
            $this->advance();
1100
            $result2 = $this->expression();
1101
            $result = $this->createTree('ptgLE', $result, $result2);
1102 18 View Code Duplication
        } elseif ($this->currentToken == '>=') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1103
            $this->advance();
1104
            $result2 = $this->expression();
1105
            $result = $this->createTree('ptgGE', $result, $result2);
1106 18
        } elseif ($this->currentToken == '=') {
1107
            $this->advance();
1108
            $result2 = $this->expression();
1109
            $result = $this->createTree('ptgEQ', $result, $result2);
1110 18 View Code Duplication
        } elseif ($this->currentToken == '<>') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1111 5
            $this->advance();
1112 5
            $result2 = $this->expression();
1113 5
            $result = $this->createTree('ptgNE', $result, $result2);
1114 18
        } elseif ($this->currentToken == '&') {
1115 6
            $this->advance();
1116 6
            $result2 = $this->expression();
1117 6
            $result = $this->createTree('ptgConcat', $result, $result2);
1118
        }
1119
1120 18
        return $result;
1121
    }
1122
1123
    /**
1124
     * It parses a expression. It assumes the following rule:
1125
     * Expr -> Term [("+" | "-") Term]
1126
     *      -> "string"
1127
     *      -> "-" Term : Negative value
1128
     *      -> "+" Term : Positive value
1129
     *      -> Error code.
1130
     *
1131
     * @return mixed The parsed ptg'd tree on success
1132
     */
1133 18
    private function expression()
1134
    {
1135
        // If it's a string return a string node
1136 18
        if (preg_match('/"([^"]|""){0,255}"/', $this->currentToken)) {
1137 9
            $tmp = str_replace('""', '"', $this->currentToken);
1138 9
            if (($tmp == '"') || ($tmp == '')) {
1139
                //    Trap for "" that has been used for an empty string
1140 5
                $tmp = '""';
1141
            }
1142 9
            $result = $this->createTree($tmp, '', '');
1143 9
            $this->advance();
1144
1145 9
            return $result;
1146
            // If it's an error code
1147 18
        } elseif (preg_match("/^#[A-Z0\/]{3,5}[!?]{1}$/", $this->currentToken) or $this->currentToken == '#N/A') {
1148
            $result = $this->createTree($this->currentToken, 'ptgErr', '');
1149
            $this->advance();
1150
1151
            return $result;
1152
            // If it's a negative value
1153 18
        } elseif ($this->currentToken == '-') {
1154
            // catch "-" Term
1155
            $this->advance();
1156
            $result2 = $this->expression();
1157
            $result = $this->createTree('ptgUminus', $result2, '');
1158
1159
            return $result;
1160
            // If it's a positive value
1161 18 View Code Duplication
        } elseif ($this->currentToken == '+') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1162
            // catch "+" Term
1163
            $this->advance();
1164
            $result2 = $this->expression();
1165
            $result = $this->createTree('ptgUplus', $result2, '');
1166
1167
            return $result;
1168
        }
1169 18
        $result = $this->term();
1170 18
        while (($this->currentToken == '+') or
1171 18
               ($this->currentToken == '-') or
1172 18
               ($this->currentToken == '^')) {
1173 10
            if ($this->currentToken == '+') {
1174 10
                $this->advance();
1175 10
                $result2 = $this->term();
1176 10
                $result = $this->createTree('ptgAdd', $result, $result2);
1177 3
            } elseif ($this->currentToken == '-') {
1178 3
                $this->advance();
1179 3
                $result2 = $this->term();
1180 3
                $result = $this->createTree('ptgSub', $result, $result2);
1181
            } else {
1182
                $this->advance();
1183
                $result2 = $this->term();
1184
                $result = $this->createTree('ptgPower', $result, $result2);
1185
            }
1186
        }
1187
1188 18
        return $result;
1189
    }
1190
1191
    /**
1192
     * This function just introduces a ptgParen element in the tree, so that Excel
1193
     * doesn't get confused when working with a parenthesized formula afterwards.
1194
     *
1195
     * @see fact()
1196
     *
1197
     * @return array The parsed ptg'd tree
1198
     */
1199 1
    private function parenthesizedExpression()
1200
    {
1201 1
        $result = $this->createTree('ptgParen', $this->expression(), '');
1202
1203 1
        return $result;
1204
    }
1205
1206
    /**
1207
     * It parses a term. It assumes the following rule:
1208
     * Term -> Fact [("*" | "/") Fact].
1209
     *
1210
     * @return mixed The parsed ptg'd tree on success
1211
     */
1212 18
    private function term()
1213
    {
1214 18
        $result = $this->fact();
1215 18
        while (($this->currentToken == '*') or
1216 18
               ($this->currentToken == '/')) {
1217 10
            if ($this->currentToken == '*') {
1218 10
                $this->advance();
1219 10
                $result2 = $this->fact();
1220 10
                $result = $this->createTree('ptgMul', $result, $result2);
1221
            } else {
1222 3
                $this->advance();
1223 3
                $result2 = $this->fact();
1224 3
                $result = $this->createTree('ptgDiv', $result, $result2);
1225
            }
1226
        }
1227
1228 18
        return $result;
1229
    }
1230
1231
    /**
1232
     * It parses a factor. It assumes the following rule:
1233
     * Fact -> ( Expr )
1234
     *       | CellRef
1235
     *       | CellRange
1236
     *       | Number
1237
     *       | Function.
1238
     *
1239
     * @return mixed The parsed ptg'd tree on success
1240
     */
1241 18
    private function fact()
1242
    {
1243 18
        if ($this->currentToken == '(') {
1244 1
            $this->advance(); // eat the "("
1245 1
            $result = $this->parenthesizedExpression();
1246 1
            if ($this->currentToken != ')') {
1247
                throw new WriterException("')' token expected.");
1248
            }
1249 1
            $this->advance(); // eat the ")"
1250 1
            return $result;
1251
        }
1252
        // if it's a reference
1253 18
        if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?[0-9]+$/', $this->currentToken)) {
1254 12
            $result = $this->createTree($this->currentToken, '', '');
1255 12
            $this->advance();
1256
1257 12
            return $result;
1258 18 View Code Duplication
        } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . "(\:" . self::REGEX_SHEET_TITLE_UNQUOTED . ")?\!\\$?[A-Ia-i]?[A-Za-z]\\$?[0-9]+$/u", $this->currentToken)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1259
            // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
1260
            $result = $this->createTree($this->currentToken, '', '');
1261
            $this->advance();
1262
1263
            return $result;
1264 18
        } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . "(\:" . self::REGEX_SHEET_TITLE_QUOTED . ")?'\!\\$?[A-Ia-i]?[A-Za-z]\\$?[0-9]+$/u", $this->currentToken)) {
1265
            // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
1266 1
            $result = $this->createTree($this->currentToken, '', '');
1267 1
            $this->advance();
1268
1269 1
            return $result;
1270 18
        } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+:(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/', $this->currentToken) or
1271 18
                preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/', $this->currentToken)) {
1272
            // if it's a range A1:B2 or $A$1:$B$2
1273
            // must be an error?
1274 15
            $result = $this->createTree($this->currentToken, '', '');
1275 15
            $this->advance();
1276
1277 15
            return $result;
1278 18 View Code Duplication
        } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . "(\:" . self::REGEX_SHEET_TITLE_UNQUOTED . ")?\!\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+:\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+$/u", $this->currentToken)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1279
            // 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)
1280
            // must be an error?
1281
            $result = $this->createTree($this->currentToken, '', '');
1282
            $this->advance();
1283
1284
            return $result;
1285 18
        } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . "(\:" . self::REGEX_SHEET_TITLE_QUOTED . ")?'\!\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+:\\$?([A-Ia-i]?[A-Za-z])?\\$?[0-9]+$/u", $this->currentToken)) {
1286
            // 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)
1287
            // must be an error?
1288 3
            $result = $this->createTree($this->currentToken, '', '');
1289 3
            $this->advance();
1290
1291 3
            return $result;
1292 18
        } elseif (is_numeric($this->currentToken)) {
1293
            // If it's a number or a percent
1294 10
            if ($this->lookAhead == '%') {
1295
                $result = $this->createTree('ptgPercent', $this->currentToken, '');
1296
                $this->advance(); // Skip the percentage operator once we've pre-built that tree
1297
            } else {
1298 10
                $result = $this->createTree($this->currentToken, '', '');
1299
            }
1300 10
            $this->advance();
1301
1302 10
            return $result;
1303 18
        } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\.]+$/i", $this->currentToken)) {
1304
            // if it's a function call
1305 15
            $result = $this->func();
1306
1307 15
            return $result;
1308
        }
1309
1310 6
        throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter);
1311
    }
1312
1313
    /**
1314
     * It parses a function call. It assumes the following rule:
1315
     * Func -> ( Expr [,Expr]* ).
1316
     *
1317
     * @return mixed The parsed ptg'd tree on success
1318
     */
1319 15
    private function func()
1320
    {
1321 15
        $num_args = 0; // number of arguments received
1322 15
        $function = strtoupper($this->currentToken);
1323 15
        $result = ''; // initialize result
1324 15
        $this->advance();
1325 15
        $this->advance(); // eat the "("
1326 15
        while ($this->currentToken != ')') {
1327 15
            if ($num_args > 0) {
1328 7
                if ($this->currentToken == ',' || $this->currentToken == ';') {
1329 7
                    $this->advance(); // eat the "," or ";"
1330
                } else {
1331
                    throw new WriterException("Syntax error: comma expected in function $function, arg #{$num_args}");
1332
                }
1333 7
                $result2 = $this->condition();
1334 7
                $result = $this->createTree('arg', $result, $result2);
1335
            } else { // first argument
1336 15
                $result2 = $this->condition();
1337 15
                $result = $this->createTree('arg', '', $result2);
1338
            }
1339 15
            ++$num_args;
1340
        }
1341 15
        if (!isset($this->functions[$function])) {
1342 1
            throw new WriterException("Function $function() doesn't exist");
1343
        }
1344 15
        $args = $this->functions[$function][1];
1345
        // If fixed number of args eg. TIME($i, $j, $k). Check that the number of args is valid.
1346 15
        if (($args >= 0) and ($args != $num_args)) {
1347
            throw new WriterException("Incorrect number of arguments in function $function() ");
1348
        }
1349
1350 15
        $result = $this->createTree($function, $result, $num_args);
1351 15
        $this->advance(); // eat the ")"
1352 15
        return $result;
1353
    }
1354
1355
    /**
1356
     * Creates a tree. In fact an array which may have one or two arrays (sub-trees)
1357
     * as elements.
1358
     *
1359
     * @param mixed $value the value of this node
1360
     * @param mixed $left the left array (sub-tree) or a final node
1361
     * @param mixed $right the right array (sub-tree) or a final node
1362
     *
1363
     * @return array A tree
1364
     */
1365 18
    private function createTree($value, $left, $right)
1366
    {
1367 18
        return ['value' => $value, 'left' => $left, 'right' => $right];
1368
    }
1369
1370
    /**
1371
     * Builds a string containing the tree in reverse polish notation (What you
1372
     * would use in a HP calculator stack).
1373
     * The following tree:.
1374
     *
1375
     *    +
1376
     *   / \
1377
     *  2   3
1378
     *
1379
     * produces: "23+"
1380
     *
1381
     * The following tree:
1382
     *
1383
     *    +
1384
     *   / \
1385
     *  3   *
1386
     *     / \
1387
     *    6   A1
1388
     *
1389
     * produces: "36A1*+"
1390
     *
1391
     * In fact all operands, functions, references, etc... are written as ptg's
1392
     *
1393
     * @param array $tree the optional tree to convert
1394
     *
1395
     * @return string The tree in reverse polish notation
1396
     */
1397 18
    public function toReversePolish($tree = [])
1398
    {
1399 18
        $polish = ''; // the string we are going to return
1400 18
        if (empty($tree)) { // If it's the first call use parseTree
1401 18
            $tree = $this->parseTree;
1402
        }
1403
1404 18 View Code Duplication
        if (is_array($tree['left'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1405 16
            $converted_tree = $this->toReversePolish($tree['left']);
1406 16
            $polish .= $converted_tree;
1407 18
        } elseif ($tree['left'] != '') { // It's a final node
1408
            $converted_tree = $this->convert($tree['left']);
1409
            $polish .= $converted_tree;
1410
        }
1411 18 View Code Duplication
        if (is_array($tree['right'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1412 16
            $converted_tree = $this->toReversePolish($tree['right']);
1413 16
            $polish .= $converted_tree;
1414 18
        } elseif ($tree['right'] != '') { // It's a final node
1415 15
            $converted_tree = $this->convert($tree['right']);
1416 15
            $polish .= $converted_tree;
1417
        }
1418
        // if it's a function convert it here (so we can set it's arguments)
1419 18
        if (preg_match("/^[A-Z0-9\xc0-\xdc\.]+$/", $tree['value']) and
1420 18
            !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value']) and
1421 18
            !preg_match("/^[A-Ia-i]?[A-Za-z](\d+)\.\.[A-Ia-i]?[A-Za-z](\d+)$/", $tree['value']) and
1422 18
            !is_numeric($tree['value']) and
1423 18
            !isset($this->ptg[$tree['value']])) {
1424
            // left subtree for a function is always an array.
1425 15
            if ($tree['left'] != '') {
1426 15
                $left_tree = $this->toReversePolish($tree['left']);
1427
            } else {
1428 4
                $left_tree = '';
1429
            }
1430
            // add it's left subtree and return.
1431 15
            return $left_tree . $this->convertFunction($tree['value'], $tree['right']);
1432
        }
1433 18
        $converted_tree = $this->convert($tree['value']);
1434
1435 18
        $polish .= $converted_tree;
1436
1437 18
        return $polish;
1438
    }
1439
}
1440