Passed
Push — master ( 95c8bb...bbbfdb )
by Adrien
29:49 queued 23:07
created

Calculation::setLocale()   D

Complexity

Conditions 19
Paths 140

Size

Total Lines 78
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 19.7517

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 47
c 1
b 0
f 0
nc 140
nop 1
dl 0
loc 78
ccs 41
cts 47
cp 0.8723
crap 19.7517
rs 4.1833

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\Calculation;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack;
6
use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger;
7
use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack;
8
use PhpOffice\PhpSpreadsheet\Cell\Cell;
9
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
10
use PhpOffice\PhpSpreadsheet\NamedRange;
11
use PhpOffice\PhpSpreadsheet\Shared;
12
use PhpOffice\PhpSpreadsheet\Spreadsheet;
13
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
14
15
class Calculation
16
{
17
    /** Constants                */
18
    /** Regular Expressions        */
19
    //    Numeric operand
20
    const CALCULATION_REGEXP_NUMBER = '[-+]?\d*\.?\d+(e[-+]?\d+)?';
21
    //    String operand
22
    const CALCULATION_REGEXP_STRING = '"(?:[^"]|"")*"';
23
    //    Opening bracket
24
    const CALCULATION_REGEXP_OPENBRACE = '\(';
25
    //    Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
26
    const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([A-Z][A-Z0-9\.]*)[\s]*\(';
27
    //    Cell reference (cell or range of cells, with or without a sheet reference)
28
    const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?([a-z]{1,3})\$?(\d{1,7})';
29
    //    Named Range of cells
30
    const CALCULATION_REGEXP_NAMEDRANGE = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?([_A-Z][_A-Z0-9\.]*)';
31
    //    Error
32
    const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?';
33
34
    /** constants */
35
    const RETURN_ARRAY_AS_ERROR = 'error';
36
    const RETURN_ARRAY_AS_VALUE = 'value';
37
    const RETURN_ARRAY_AS_ARRAY = 'array';
38
39
    private static $returnArrayAsType = self::RETURN_ARRAY_AS_VALUE;
40
41
    /**
42
     * Instance of this class.
43
     *
44
     * @var Calculation
45
     */
46
    private static $instance;
47
48
    /**
49
     * Instance of the spreadsheet this Calculation Engine is using.
50
     *
51
     * @var Spreadsheet
52
     */
53
    private $spreadsheet;
54
55
    /**
56
     * Calculation cache.
57
     *
58
     * @var array
59
     */
60
    private $calculationCache = [];
61
62
    /**
63
     * Calculation cache enabled.
64
     *
65
     * @var bool
66
     */
67
    private $calculationCacheEnabled = true;
68
69
    /**
70
     * Used to generate unique store keys.
71
     *
72
     * @var int
73
     */
74
    private $branchStoreKeyCounter = 0;
75
76
    private $branchPruningEnabled = true;
77
78
    /**
79
     * List of operators that can be used within formulae
80
     * The true/false value indicates whether it is a binary operator or a unary operator.
81
     *
82
     * @var array
83
     */
84
    private static $operators = [
85
        '+' => true, '-' => true, '*' => true, '/' => true,
86
        '^' => true, '&' => true, '%' => false, '~' => false,
87
        '>' => true, '<' => true, '=' => true, '>=' => true,
88
        '<=' => true, '<>' => true, '|' => true, ':' => true,
89
    ];
90
91
    /**
92
     * List of binary operators (those that expect two operands).
93
     *
94
     * @var array
95
     */
96
    private static $binaryOperators = [
97
        '+' => true, '-' => true, '*' => true, '/' => true,
98
        '^' => true, '&' => true, '>' => true, '<' => true,
99
        '=' => true, '>=' => true, '<=' => true, '<>' => true,
100
        '|' => true, ':' => true,
101
    ];
102
103
    /**
104
     * The debug log generated by the calculation engine.
105
     *
106
     * @var Logger
107
     */
108
    private $debugLog;
109
110
    /**
111
     * Flag to determine how formula errors should be handled
112
     *        If true, then a user error will be triggered
113
     *        If false, then an exception will be thrown.
114
     *
115
     * @var bool
116
     */
117
    public $suppressFormulaErrors = false;
118
119
    /**
120
     * Error message for any error that was raised/thrown by the calculation engine.
121
     *
122
     * @var string
123
     */
124
    public $formulaError;
125
126
    /**
127
     * An array of the nested cell references accessed by the calculation engine, used for the debug log.
128
     *
129
     * @var CyclicReferenceStack
130
     */
131
    private $cyclicReferenceStack;
132
133
    private $cellStack = [];
134
135
    /**
136
     * Current iteration counter for cyclic formulae
137
     * If the value is 0 (or less) then cyclic formulae will throw an exception,
138
     * otherwise they will iterate to the limit defined here before returning a result.
139
     *
140
     * @var int
141
     */
142
    private $cyclicFormulaCounter = 1;
143
144
    private $cyclicFormulaCell = '';
145
146
    /**
147
     * Number of iterations for cyclic formulae.
148
     *
149
     * @var int
150
     */
151
    public $cyclicFormulaCount = 1;
152
153
    /**
154
     * Epsilon Precision used for comparisons in calculations.
155
     *
156
     * @var float
157
     */
158
    private $delta = 0.1e-12;
159
160
    /**
161
     * The current locale setting.
162
     *
163
     * @var string
164
     */
165
    private static $localeLanguage = 'en_us'; //    US English    (default locale)
166
167
    /**
168
     * List of available locale settings
169
     * Note that this is read for the locale subdirectory only when requested.
170
     *
171
     * @var string[]
172
     */
173
    private static $validLocaleLanguages = [
174
        'en', //    English        (default language)
175
    ];
176
177
    /**
178
     * Locale-specific argument separator for function arguments.
179
     *
180
     * @var string
181
     */
182
    private static $localeArgumentSeparator = ',';
183
184
    private static $localeFunctions = [];
185
186
    /**
187
     * Locale-specific translations for Excel constants (True, False and Null).
188
     *
189
     * @var string[]
190
     */
191
    public static $localeBoolean = [
192
        'TRUE' => 'TRUE',
193
        'FALSE' => 'FALSE',
194
        'NULL' => 'NULL',
195
    ];
196
197
    /**
198
     * Excel constant string translations to their PHP equivalents
199
     * Constant conversion from text name/value to actual (datatyped) value.
200
     *
201
     * @var string[]
202
     */
203
    private static $excelConstants = [
204
        'TRUE' => true,
205
        'FALSE' => false,
206
        'NULL' => null,
207
    ];
208
209
    // PhpSpreadsheet functions
210
    private static $phpSpreadsheetFunctions = [
211
        'ABS' => [
212
            'category' => Category::CATEGORY_MATH_AND_TRIG,
213
            'functionCall' => 'abs',
214
            'argumentCount' => '1',
215
        ],
216
        'ACCRINT' => [
217
            'category' => Category::CATEGORY_FINANCIAL,
218
            'functionCall' => [Financial::class, 'ACCRINT'],
219
            'argumentCount' => '4-7',
220
        ],
221
        'ACCRINTM' => [
222
            'category' => Category::CATEGORY_FINANCIAL,
223
            'functionCall' => [Financial::class, 'ACCRINTM'],
224
            'argumentCount' => '3-5',
225
        ],
226
        'ACOS' => [
227
            'category' => Category::CATEGORY_MATH_AND_TRIG,
228
            'functionCall' => 'acos',
229
            'argumentCount' => '1',
230
        ],
231
        'ACOSH' => [
232
            'category' => Category::CATEGORY_MATH_AND_TRIG,
233
            'functionCall' => 'acosh',
234
            'argumentCount' => '1',
235
        ],
236
        'ACOT' => [
237
            'category' => Category::CATEGORY_MATH_AND_TRIG,
238
            'functionCall' => [MathTrig::class, 'ACOT'],
239
            'argumentCount' => '1',
240
        ],
241
        'ACOTH' => [
242
            'category' => Category::CATEGORY_MATH_AND_TRIG,
243
            'functionCall' => [MathTrig::class, 'ACOTH'],
244
            'argumentCount' => '1',
245
        ],
246
        'ADDRESS' => [
247
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
248
            'functionCall' => [LookupRef::class, 'cellAddress'],
249
            'argumentCount' => '2-5',
250
        ],
251
        'AMORDEGRC' => [
252
            'category' => Category::CATEGORY_FINANCIAL,
253
            'functionCall' => [Financial::class, 'AMORDEGRC'],
254
            'argumentCount' => '6,7',
255
        ],
256
        'AMORLINC' => [
257
            'category' => Category::CATEGORY_FINANCIAL,
258
            'functionCall' => [Financial::class, 'AMORLINC'],
259
            'argumentCount' => '6,7',
260
        ],
261
        'AND' => [
262
            'category' => Category::CATEGORY_LOGICAL,
263
            'functionCall' => [Logical::class, 'logicalAnd'],
264
            'argumentCount' => '1+',
265
        ],
266
        'AREAS' => [
267
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
268
            'functionCall' => [Functions::class, 'DUMMY'],
269
            'argumentCount' => '1',
270
        ],
271
        'ASC' => [
272
            'category' => Category::CATEGORY_TEXT_AND_DATA,
273
            'functionCall' => [Functions::class, 'DUMMY'],
274
            'argumentCount' => '1',
275
        ],
276
        'ASIN' => [
277
            'category' => Category::CATEGORY_MATH_AND_TRIG,
278
            'functionCall' => 'asin',
279
            'argumentCount' => '1',
280
        ],
281
        'ASINH' => [
282
            'category' => Category::CATEGORY_MATH_AND_TRIG,
283
            'functionCall' => 'asinh',
284
            'argumentCount' => '1',
285
        ],
286
        'ATAN' => [
287
            'category' => Category::CATEGORY_MATH_AND_TRIG,
288
            'functionCall' => 'atan',
289
            'argumentCount' => '1',
290
        ],
291
        'ATAN2' => [
292
            'category' => Category::CATEGORY_MATH_AND_TRIG,
293
            'functionCall' => [MathTrig::class, 'ATAN2'],
294
            'argumentCount' => '2',
295
        ],
296
        'ATANH' => [
297
            'category' => Category::CATEGORY_MATH_AND_TRIG,
298
            'functionCall' => 'atanh',
299
            'argumentCount' => '1',
300
        ],
301
        'AVEDEV' => [
302
            'category' => Category::CATEGORY_STATISTICAL,
303
            'functionCall' => [Statistical::class, 'AVEDEV'],
304
            'argumentCount' => '1+',
305
        ],
306
        'AVERAGE' => [
307
            'category' => Category::CATEGORY_STATISTICAL,
308
            'functionCall' => [Statistical::class, 'AVERAGE'],
309
            'argumentCount' => '1+',
310
        ],
311
        'AVERAGEA' => [
312
            'category' => Category::CATEGORY_STATISTICAL,
313
            'functionCall' => [Statistical::class, 'AVERAGEA'],
314
            'argumentCount' => '1+',
315
        ],
316
        'AVERAGEIF' => [
317
            'category' => Category::CATEGORY_STATISTICAL,
318
            'functionCall' => [Statistical::class, 'AVERAGEIF'],
319
            'argumentCount' => '2,3',
320
        ],
321
        'AVERAGEIFS' => [
322
            'category' => Category::CATEGORY_STATISTICAL,
323
            'functionCall' => [Functions::class, 'DUMMY'],
324
            'argumentCount' => '3+',
325
        ],
326
        'BAHTTEXT' => [
327
            'category' => Category::CATEGORY_TEXT_AND_DATA,
328
            'functionCall' => [Functions::class, 'DUMMY'],
329
            'argumentCount' => '1',
330
        ],
331
        'BESSELI' => [
332
            'category' => Category::CATEGORY_ENGINEERING,
333
            'functionCall' => [Engineering::class, 'BESSELI'],
334
            'argumentCount' => '2',
335
        ],
336
        'BESSELJ' => [
337
            'category' => Category::CATEGORY_ENGINEERING,
338
            'functionCall' => [Engineering::class, 'BESSELJ'],
339
            'argumentCount' => '2',
340
        ],
341
        'BESSELK' => [
342
            'category' => Category::CATEGORY_ENGINEERING,
343
            'functionCall' => [Engineering::class, 'BESSELK'],
344
            'argumentCount' => '2',
345
        ],
346
        'BESSELY' => [
347
            'category' => Category::CATEGORY_ENGINEERING,
348
            'functionCall' => [Engineering::class, 'BESSELY'],
349
            'argumentCount' => '2',
350
        ],
351
        'BETADIST' => [
352
            'category' => Category::CATEGORY_STATISTICAL,
353
            'functionCall' => [Statistical::class, 'BETADIST'],
354
            'argumentCount' => '3-5',
355
        ],
356
        'BETAINV' => [
357
            'category' => Category::CATEGORY_STATISTICAL,
358
            'functionCall' => [Statistical::class, 'BETAINV'],
359
            'argumentCount' => '3-5',
360
        ],
361
        'BIN2DEC' => [
362
            'category' => Category::CATEGORY_ENGINEERING,
363
            'functionCall' => [Engineering::class, 'BINTODEC'],
364
            'argumentCount' => '1',
365
        ],
366
        'BIN2HEX' => [
367
            'category' => Category::CATEGORY_ENGINEERING,
368
            'functionCall' => [Engineering::class, 'BINTOHEX'],
369
            'argumentCount' => '1,2',
370
        ],
371
        'BIN2OCT' => [
372
            'category' => Category::CATEGORY_ENGINEERING,
373
            'functionCall' => [Engineering::class, 'BINTOOCT'],
374
            'argumentCount' => '1,2',
375
        ],
376
        'BINOMDIST' => [
377
            'category' => Category::CATEGORY_STATISTICAL,
378
            'functionCall' => [Statistical::class, 'BINOMDIST'],
379
            'argumentCount' => '4',
380
        ],
381
        'BITAND' => [
382
            'category' => Category::CATEGORY_ENGINEERING,
383
            'functionCall' => [Engineering::class, 'BITAND'],
384
            'argumentCount' => '2',
385
        ],
386
        'BITOR' => [
387
            'category' => Category::CATEGORY_ENGINEERING,
388
            'functionCall' => [Engineering::class, 'BITOR'],
389
            'argumentCount' => '2',
390
        ],
391
        'BITXOR' => [
392
            'category' => Category::CATEGORY_ENGINEERING,
393
            'functionCall' => [Engineering::class, 'BITOR'],
394
            'argumentCount' => '2',
395
        ],
396
        'BITLSHIFT' => [
397
            'category' => Category::CATEGORY_ENGINEERING,
398
            'functionCall' => [Engineering::class, 'BITLSHIFT'],
399
            'argumentCount' => '2',
400
        ],
401
        'BITRSHIFT' => [
402
            'category' => Category::CATEGORY_ENGINEERING,
403
            'functionCall' => [Engineering::class, 'BITRSHIFT'],
404
            'argumentCount' => '2',
405
        ],
406
        'CEILING' => [
407
            'category' => Category::CATEGORY_MATH_AND_TRIG,
408
            'functionCall' => [MathTrig::class, 'CEILING'],
409
            'argumentCount' => '2',
410
        ],
411
        'CELL' => [
412
            'category' => Category::CATEGORY_INFORMATION,
413
            'functionCall' => [Functions::class, 'DUMMY'],
414
            'argumentCount' => '1,2',
415
        ],
416
        'CHAR' => [
417
            'category' => Category::CATEGORY_TEXT_AND_DATA,
418
            'functionCall' => [TextData::class, 'CHARACTER'],
419
            'argumentCount' => '1',
420
        ],
421
        'CHIDIST' => [
422
            'category' => Category::CATEGORY_STATISTICAL,
423
            'functionCall' => [Statistical::class, 'CHIDIST'],
424
            'argumentCount' => '2',
425
        ],
426
        'CHIINV' => [
427
            'category' => Category::CATEGORY_STATISTICAL,
428
            'functionCall' => [Statistical::class, 'CHIINV'],
429
            'argumentCount' => '2',
430
        ],
431
        'CHITEST' => [
432
            'category' => Category::CATEGORY_STATISTICAL,
433
            'functionCall' => [Functions::class, 'DUMMY'],
434
            'argumentCount' => '2',
435
        ],
436
        'CHOOSE' => [
437
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
438
            'functionCall' => [LookupRef::class, 'CHOOSE'],
439
            'argumentCount' => '2+',
440
        ],
441
        'CLEAN' => [
442
            'category' => Category::CATEGORY_TEXT_AND_DATA,
443
            'functionCall' => [TextData::class, 'TRIMNONPRINTABLE'],
444
            'argumentCount' => '1',
445
        ],
446
        'CODE' => [
447
            'category' => Category::CATEGORY_TEXT_AND_DATA,
448
            'functionCall' => [TextData::class, 'ASCIICODE'],
449
            'argumentCount' => '1',
450
        ],
451
        'COLUMN' => [
452
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
453
            'functionCall' => [LookupRef::class, 'COLUMN'],
454
            'argumentCount' => '-1',
455
            'passByReference' => [true],
456
        ],
457
        'COLUMNS' => [
458
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
459
            'functionCall' => [LookupRef::class, 'COLUMNS'],
460
            'argumentCount' => '1',
461
        ],
462
        'COMBIN' => [
463
            'category' => Category::CATEGORY_MATH_AND_TRIG,
464
            'functionCall' => [MathTrig::class, 'COMBIN'],
465
            'argumentCount' => '2',
466
        ],
467
        'COMPLEX' => [
468
            'category' => Category::CATEGORY_ENGINEERING,
469
            'functionCall' => [Engineering::class, 'COMPLEX'],
470
            'argumentCount' => '2,3',
471
        ],
472
        'CONCAT' => [
473
            'category' => Category::CATEGORY_TEXT_AND_DATA,
474
            'functionCall' => [TextData::class, 'CONCATENATE'],
475
            'argumentCount' => '1+',
476
        ],
477
        'CONCATENATE' => [
478
            'category' => Category::CATEGORY_TEXT_AND_DATA,
479
            'functionCall' => [TextData::class, 'CONCATENATE'],
480
            'argumentCount' => '1+',
481
        ],
482
        'CONFIDENCE' => [
483
            'category' => Category::CATEGORY_STATISTICAL,
484
            'functionCall' => [Statistical::class, 'CONFIDENCE'],
485
            'argumentCount' => '3',
486
        ],
487
        'CONVERT' => [
488
            'category' => Category::CATEGORY_ENGINEERING,
489
            'functionCall' => [Engineering::class, 'CONVERTUOM'],
490
            'argumentCount' => '3',
491
        ],
492
        'CORREL' => [
493
            'category' => Category::CATEGORY_STATISTICAL,
494
            'functionCall' => [Statistical::class, 'CORREL'],
495
            'argumentCount' => '2',
496
        ],
497
        'COS' => [
498
            'category' => Category::CATEGORY_MATH_AND_TRIG,
499
            'functionCall' => 'cos',
500
            'argumentCount' => '1',
501
        ],
502
        'COSH' => [
503
            'category' => Category::CATEGORY_MATH_AND_TRIG,
504
            'functionCall' => 'cosh',
505
            'argumentCount' => '1',
506
        ],
507
        'COT' => [
508
            'category' => Category::CATEGORY_MATH_AND_TRIG,
509
            'functionCall' => [MathTrig::class, 'COT'],
510
            'argumentCount' => '1',
511
        ],
512
        'COTH' => [
513
            'category' => Category::CATEGORY_MATH_AND_TRIG,
514
            'functionCall' => [MathTrig::class, 'COTH'],
515
            'argumentCount' => '1',
516
        ],
517
        'COUNT' => [
518
            'category' => Category::CATEGORY_STATISTICAL,
519
            'functionCall' => [Statistical::class, 'COUNT'],
520
            'argumentCount' => '1+',
521
        ],
522
        'COUNTA' => [
523
            'category' => Category::CATEGORY_STATISTICAL,
524
            'functionCall' => [Statistical::class, 'COUNTA'],
525
            'argumentCount' => '1+',
526
        ],
527
        'COUNTBLANK' => [
528
            'category' => Category::CATEGORY_STATISTICAL,
529
            'functionCall' => [Statistical::class, 'COUNTBLANK'],
530
            'argumentCount' => '1',
531
        ],
532
        'COUNTIF' => [
533
            'category' => Category::CATEGORY_STATISTICAL,
534
            'functionCall' => [Statistical::class, 'COUNTIF'],
535
            'argumentCount' => '2',
536
        ],
537
        'COUNTIFS' => [
538
            'category' => Category::CATEGORY_STATISTICAL,
539
            'functionCall' => [Statistical::class, 'COUNTIFS'],
540
            'argumentCount' => '2+',
541
        ],
542
        'COUPDAYBS' => [
543
            'category' => Category::CATEGORY_FINANCIAL,
544
            'functionCall' => [Financial::class, 'COUPDAYBS'],
545
            'argumentCount' => '3,4',
546
        ],
547
        'COUPDAYS' => [
548
            'category' => Category::CATEGORY_FINANCIAL,
549
            'functionCall' => [Financial::class, 'COUPDAYS'],
550
            'argumentCount' => '3,4',
551
        ],
552
        'COUPDAYSNC' => [
553
            'category' => Category::CATEGORY_FINANCIAL,
554
            'functionCall' => [Financial::class, 'COUPDAYSNC'],
555
            'argumentCount' => '3,4',
556
        ],
557
        'COUPNCD' => [
558
            'category' => Category::CATEGORY_FINANCIAL,
559
            'functionCall' => [Financial::class, 'COUPNCD'],
560
            'argumentCount' => '3,4',
561
        ],
562
        'COUPNUM' => [
563
            'category' => Category::CATEGORY_FINANCIAL,
564
            'functionCall' => [Financial::class, 'COUPNUM'],
565
            'argumentCount' => '3,4',
566
        ],
567
        'COUPPCD' => [
568
            'category' => Category::CATEGORY_FINANCIAL,
569
            'functionCall' => [Financial::class, 'COUPPCD'],
570
            'argumentCount' => '3,4',
571
        ],
572
        'COVAR' => [
573
            'category' => Category::CATEGORY_STATISTICAL,
574
            'functionCall' => [Statistical::class, 'COVAR'],
575
            'argumentCount' => '2',
576
        ],
577
        'CRITBINOM' => [
578
            'category' => Category::CATEGORY_STATISTICAL,
579
            'functionCall' => [Statistical::class, 'CRITBINOM'],
580
            'argumentCount' => '3',
581
        ],
582
        'CSC' => [
583
            'category' => Category::CATEGORY_MATH_AND_TRIG,
584
            'functionCall' => [MathTrig::class, 'CSC'],
585
            'argumentCount' => '1',
586
        ],
587
        'CSCH' => [
588
            'category' => Category::CATEGORY_MATH_AND_TRIG,
589
            'functionCall' => [MathTrig::class, 'CSCH'],
590
            'argumentCount' => '1',
591
        ],
592
        'CUBEKPIMEMBER' => [
593
            'category' => Category::CATEGORY_CUBE,
594
            'functionCall' => [Functions::class, 'DUMMY'],
595
            'argumentCount' => '?',
596
        ],
597
        'CUBEMEMBER' => [
598
            'category' => Category::CATEGORY_CUBE,
599
            'functionCall' => [Functions::class, 'DUMMY'],
600
            'argumentCount' => '?',
601
        ],
602
        'CUBEMEMBERPROPERTY' => [
603
            'category' => Category::CATEGORY_CUBE,
604
            'functionCall' => [Functions::class, 'DUMMY'],
605
            'argumentCount' => '?',
606
        ],
607
        'CUBERANKEDMEMBER' => [
608
            'category' => Category::CATEGORY_CUBE,
609
            'functionCall' => [Functions::class, 'DUMMY'],
610
            'argumentCount' => '?',
611
        ],
612
        'CUBESET' => [
613
            'category' => Category::CATEGORY_CUBE,
614
            'functionCall' => [Functions::class, 'DUMMY'],
615
            'argumentCount' => '?',
616
        ],
617
        'CUBESETCOUNT' => [
618
            'category' => Category::CATEGORY_CUBE,
619
            'functionCall' => [Functions::class, 'DUMMY'],
620
            'argumentCount' => '?',
621
        ],
622
        'CUBEVALUE' => [
623
            'category' => Category::CATEGORY_CUBE,
624
            'functionCall' => [Functions::class, 'DUMMY'],
625
            'argumentCount' => '?',
626
        ],
627
        'CUMIPMT' => [
628
            'category' => Category::CATEGORY_FINANCIAL,
629
            'functionCall' => [Financial::class, 'CUMIPMT'],
630
            'argumentCount' => '6',
631
        ],
632
        'CUMPRINC' => [
633
            'category' => Category::CATEGORY_FINANCIAL,
634
            'functionCall' => [Financial::class, 'CUMPRINC'],
635
            'argumentCount' => '6',
636
        ],
637
        'DATE' => [
638
            'category' => Category::CATEGORY_DATE_AND_TIME,
639
            'functionCall' => [DateTime::class, 'DATE'],
640
            'argumentCount' => '3',
641
        ],
642
        'DATEDIF' => [
643
            'category' => Category::CATEGORY_DATE_AND_TIME,
644
            'functionCall' => [DateTime::class, 'DATEDIF'],
645
            'argumentCount' => '2,3',
646
        ],
647
        'DATEVALUE' => [
648
            'category' => Category::CATEGORY_DATE_AND_TIME,
649
            'functionCall' => [DateTime::class, 'DATEVALUE'],
650
            'argumentCount' => '1',
651
        ],
652
        'DAVERAGE' => [
653
            'category' => Category::CATEGORY_DATABASE,
654
            'functionCall' => [Database::class, 'DAVERAGE'],
655
            'argumentCount' => '3',
656
        ],
657
        'DAY' => [
658
            'category' => Category::CATEGORY_DATE_AND_TIME,
659
            'functionCall' => [DateTime::class, 'DAYOFMONTH'],
660
            'argumentCount' => '1',
661
        ],
662
        'DAYS' => [
663
            'category' => Category::CATEGORY_DATE_AND_TIME,
664
            'functionCall' => [DateTime::class, 'DAYS'],
665
            'argumentCount' => '2',
666
        ],
667
        'DAYS360' => [
668
            'category' => Category::CATEGORY_DATE_AND_TIME,
669
            'functionCall' => [DateTime::class, 'DAYS360'],
670
            'argumentCount' => '2,3',
671
        ],
672
        'DB' => [
673
            'category' => Category::CATEGORY_FINANCIAL,
674
            'functionCall' => [Financial::class, 'DB'],
675
            'argumentCount' => '4,5',
676
        ],
677
        'DCOUNT' => [
678
            'category' => Category::CATEGORY_DATABASE,
679
            'functionCall' => [Database::class, 'DCOUNT'],
680
            'argumentCount' => '3',
681
        ],
682
        'DCOUNTA' => [
683
            'category' => Category::CATEGORY_DATABASE,
684
            'functionCall' => [Database::class, 'DCOUNTA'],
685
            'argumentCount' => '3',
686
        ],
687
        'DDB' => [
688
            'category' => Category::CATEGORY_FINANCIAL,
689
            'functionCall' => [Financial::class, 'DDB'],
690
            'argumentCount' => '4,5',
691
        ],
692
        'DEC2BIN' => [
693
            'category' => Category::CATEGORY_ENGINEERING,
694
            'functionCall' => [Engineering::class, 'DECTOBIN'],
695
            'argumentCount' => '1,2',
696
        ],
697
        'DEC2HEX' => [
698
            'category' => Category::CATEGORY_ENGINEERING,
699
            'functionCall' => [Engineering::class, 'DECTOHEX'],
700
            'argumentCount' => '1,2',
701
        ],
702
        'DEC2OCT' => [
703
            'category' => Category::CATEGORY_ENGINEERING,
704
            'functionCall' => [Engineering::class, 'DECTOOCT'],
705
            'argumentCount' => '1,2',
706
        ],
707
        'DEGREES' => [
708
            'category' => Category::CATEGORY_MATH_AND_TRIG,
709
            'functionCall' => 'rad2deg',
710
            'argumentCount' => '1',
711
        ],
712
        'DELTA' => [
713
            'category' => Category::CATEGORY_ENGINEERING,
714
            'functionCall' => [Engineering::class, 'DELTA'],
715
            'argumentCount' => '1,2',
716
        ],
717
        'DEVSQ' => [
718
            'category' => Category::CATEGORY_STATISTICAL,
719
            'functionCall' => [Statistical::class, 'DEVSQ'],
720
            'argumentCount' => '1+',
721
        ],
722
        'DGET' => [
723
            'category' => Category::CATEGORY_DATABASE,
724
            'functionCall' => [Database::class, 'DGET'],
725
            'argumentCount' => '3',
726
        ],
727
        'DISC' => [
728
            'category' => Category::CATEGORY_FINANCIAL,
729
            'functionCall' => [Financial::class, 'DISC'],
730
            'argumentCount' => '4,5',
731
        ],
732
        'DMAX' => [
733
            'category' => Category::CATEGORY_DATABASE,
734
            'functionCall' => [Database::class, 'DMAX'],
735
            'argumentCount' => '3',
736
        ],
737
        'DMIN' => [
738
            'category' => Category::CATEGORY_DATABASE,
739
            'functionCall' => [Database::class, 'DMIN'],
740
            'argumentCount' => '3',
741
        ],
742
        'DOLLAR' => [
743
            'category' => Category::CATEGORY_TEXT_AND_DATA,
744
            'functionCall' => [TextData::class, 'DOLLAR'],
745
            'argumentCount' => '1,2',
746
        ],
747
        'DOLLARDE' => [
748
            'category' => Category::CATEGORY_FINANCIAL,
749
            'functionCall' => [Financial::class, 'DOLLARDE'],
750
            'argumentCount' => '2',
751
        ],
752
        'DOLLARFR' => [
753
            'category' => Category::CATEGORY_FINANCIAL,
754
            'functionCall' => [Financial::class, 'DOLLARFR'],
755
            'argumentCount' => '2',
756
        ],
757
        'DPRODUCT' => [
758
            'category' => Category::CATEGORY_DATABASE,
759
            'functionCall' => [Database::class, 'DPRODUCT'],
760
            'argumentCount' => '3',
761
        ],
762
        'DSTDEV' => [
763
            'category' => Category::CATEGORY_DATABASE,
764
            'functionCall' => [Database::class, 'DSTDEV'],
765
            'argumentCount' => '3',
766
        ],
767
        'DSTDEVP' => [
768
            'category' => Category::CATEGORY_DATABASE,
769
            'functionCall' => [Database::class, 'DSTDEVP'],
770
            'argumentCount' => '3',
771
        ],
772
        'DSUM' => [
773
            'category' => Category::CATEGORY_DATABASE,
774
            'functionCall' => [Database::class, 'DSUM'],
775
            'argumentCount' => '3',
776
        ],
777
        'DURATION' => [
778
            'category' => Category::CATEGORY_FINANCIAL,
779
            'functionCall' => [Functions::class, 'DUMMY'],
780
            'argumentCount' => '5,6',
781
        ],
782
        'DVAR' => [
783
            'category' => Category::CATEGORY_DATABASE,
784
            'functionCall' => [Database::class, 'DVAR'],
785
            'argumentCount' => '3',
786
        ],
787
        'DVARP' => [
788
            'category' => Category::CATEGORY_DATABASE,
789
            'functionCall' => [Database::class, 'DVARP'],
790
            'argumentCount' => '3',
791
        ],
792
        'EDATE' => [
793
            'category' => Category::CATEGORY_DATE_AND_TIME,
794
            'functionCall' => [DateTime::class, 'EDATE'],
795
            'argumentCount' => '2',
796
        ],
797
        'EFFECT' => [
798
            'category' => Category::CATEGORY_FINANCIAL,
799
            'functionCall' => [Financial::class, 'EFFECT'],
800
            'argumentCount' => '2',
801
        ],
802
        'EOMONTH' => [
803
            'category' => Category::CATEGORY_DATE_AND_TIME,
804
            'functionCall' => [DateTime::class, 'EOMONTH'],
805
            'argumentCount' => '2',
806
        ],
807
        'ERF' => [
808
            'category' => Category::CATEGORY_ENGINEERING,
809
            'functionCall' => [Engineering::class, 'ERF'],
810
            'argumentCount' => '1,2',
811
        ],
812
        'ERF.PRECISE' => [
813
            'category' => Category::CATEGORY_ENGINEERING,
814
            'functionCall' => [Engineering::class, 'ERFPRECISE'],
815
            'argumentCount' => '1',
816
        ],
817
        'ERFC' => [
818
            'category' => Category::CATEGORY_ENGINEERING,
819
            'functionCall' => [Engineering::class, 'ERFC'],
820
            'argumentCount' => '1',
821
        ],
822
        'ERFC.PRECISE' => [
823
            'category' => Category::CATEGORY_ENGINEERING,
824
            'functionCall' => [Engineering::class, 'ERFC'],
825
            'argumentCount' => '1',
826
        ],
827
        'ERROR.TYPE' => [
828
            'category' => Category::CATEGORY_INFORMATION,
829
            'functionCall' => [Functions::class, 'errorType'],
830
            'argumentCount' => '1',
831
        ],
832
        'EVEN' => [
833
            'category' => Category::CATEGORY_MATH_AND_TRIG,
834
            'functionCall' => [MathTrig::class, 'EVEN'],
835
            'argumentCount' => '1',
836
        ],
837
        'EXACT' => [
838
            'category' => Category::CATEGORY_TEXT_AND_DATA,
839
            'functionCall' => [TextData::class, 'EXACT'],
840
            'argumentCount' => '2',
841
        ],
842
        'EXP' => [
843
            'category' => Category::CATEGORY_MATH_AND_TRIG,
844
            'functionCall' => 'exp',
845
            'argumentCount' => '1',
846
        ],
847
        'EXPONDIST' => [
848
            'category' => Category::CATEGORY_STATISTICAL,
849
            'functionCall' => [Statistical::class, 'EXPONDIST'],
850
            'argumentCount' => '3',
851
        ],
852
        'FACT' => [
853
            'category' => Category::CATEGORY_MATH_AND_TRIG,
854
            'functionCall' => [MathTrig::class, 'FACT'],
855
            'argumentCount' => '1',
856
        ],
857
        'FACTDOUBLE' => [
858
            'category' => Category::CATEGORY_MATH_AND_TRIG,
859
            'functionCall' => [MathTrig::class, 'FACTDOUBLE'],
860
            'argumentCount' => '1',
861
        ],
862
        'FALSE' => [
863
            'category' => Category::CATEGORY_LOGICAL,
864
            'functionCall' => [Logical::class, 'FALSE'],
865
            'argumentCount' => '0',
866
        ],
867
        'FDIST' => [
868
            'category' => Category::CATEGORY_STATISTICAL,
869
            'functionCall' => [Functions::class, 'DUMMY'],
870
            'argumentCount' => '3',
871
        ],
872
        'FIND' => [
873
            'category' => Category::CATEGORY_TEXT_AND_DATA,
874
            'functionCall' => [TextData::class, 'SEARCHSENSITIVE'],
875
            'argumentCount' => '2,3',
876
        ],
877
        'FINDB' => [
878
            'category' => Category::CATEGORY_TEXT_AND_DATA,
879
            'functionCall' => [TextData::class, 'SEARCHSENSITIVE'],
880
            'argumentCount' => '2,3',
881
        ],
882
        'FINV' => [
883
            'category' => Category::CATEGORY_STATISTICAL,
884
            'functionCall' => [Functions::class, 'DUMMY'],
885
            'argumentCount' => '3',
886
        ],
887
        'FISHER' => [
888
            'category' => Category::CATEGORY_STATISTICAL,
889
            'functionCall' => [Statistical::class, 'FISHER'],
890
            'argumentCount' => '1',
891
        ],
892
        'FISHERINV' => [
893
            'category' => Category::CATEGORY_STATISTICAL,
894
            'functionCall' => [Statistical::class, 'FISHERINV'],
895
            'argumentCount' => '1',
896
        ],
897
        'FIXED' => [
898
            'category' => Category::CATEGORY_TEXT_AND_DATA,
899
            'functionCall' => [TextData::class, 'FIXEDFORMAT'],
900
            'argumentCount' => '1-3',
901
        ],
902
        'FLOOR' => [
903
            'category' => Category::CATEGORY_MATH_AND_TRIG,
904
            'functionCall' => [MathTrig::class, 'FLOOR'],
905
            'argumentCount' => '2',
906
        ],
907
        'FORECAST' => [
908
            'category' => Category::CATEGORY_STATISTICAL,
909
            'functionCall' => [Statistical::class, 'FORECAST'],
910
            'argumentCount' => '3',
911
        ],
912
        'FORMULATEXT' => [
913
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
914
            'functionCall' => [LookupRef::class, 'FORMULATEXT'],
915
            'argumentCount' => '1',
916
            'passCellReference' => true,
917
            'passByReference' => [true],
918
        ],
919
        'FREQUENCY' => [
920
            'category' => Category::CATEGORY_STATISTICAL,
921
            'functionCall' => [Functions::class, 'DUMMY'],
922
            'argumentCount' => '2',
923
        ],
924
        'FTEST' => [
925
            'category' => Category::CATEGORY_STATISTICAL,
926
            'functionCall' => [Functions::class, 'DUMMY'],
927
            'argumentCount' => '2',
928
        ],
929
        'FV' => [
930
            'category' => Category::CATEGORY_FINANCIAL,
931
            'functionCall' => [Financial::class, 'FV'],
932
            'argumentCount' => '3-5',
933
        ],
934
        'FVSCHEDULE' => [
935
            'category' => Category::CATEGORY_FINANCIAL,
936
            'functionCall' => [Financial::class, 'FVSCHEDULE'],
937
            'argumentCount' => '2',
938
        ],
939
        'GAMMADIST' => [
940
            'category' => Category::CATEGORY_STATISTICAL,
941
            'functionCall' => [Statistical::class, 'GAMMADIST'],
942
            'argumentCount' => '4',
943
        ],
944
        'GAMMAINV' => [
945
            'category' => Category::CATEGORY_STATISTICAL,
946
            'functionCall' => [Statistical::class, 'GAMMAINV'],
947
            'argumentCount' => '3',
948
        ],
949
        'GAMMALN' => [
950
            'category' => Category::CATEGORY_STATISTICAL,
951
            'functionCall' => [Statistical::class, 'GAMMALN'],
952
            'argumentCount' => '1',
953
        ],
954
        'GCD' => [
955
            'category' => Category::CATEGORY_MATH_AND_TRIG,
956
            'functionCall' => [MathTrig::class, 'GCD'],
957
            'argumentCount' => '1+',
958
        ],
959
        'GEOMEAN' => [
960
            'category' => Category::CATEGORY_STATISTICAL,
961
            'functionCall' => [Statistical::class, 'GEOMEAN'],
962
            'argumentCount' => '1+',
963
        ],
964
        'GESTEP' => [
965
            'category' => Category::CATEGORY_ENGINEERING,
966
            'functionCall' => [Engineering::class, 'GESTEP'],
967
            'argumentCount' => '1,2',
968
        ],
969
        'GETPIVOTDATA' => [
970
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
971
            'functionCall' => [Functions::class, 'DUMMY'],
972
            'argumentCount' => '2+',
973
        ],
974
        'GROWTH' => [
975
            'category' => Category::CATEGORY_STATISTICAL,
976
            'functionCall' => [Statistical::class, 'GROWTH'],
977
            'argumentCount' => '1-4',
978
        ],
979
        'HARMEAN' => [
980
            'category' => Category::CATEGORY_STATISTICAL,
981
            'functionCall' => [Statistical::class, 'HARMEAN'],
982
            'argumentCount' => '1+',
983
        ],
984
        'HEX2BIN' => [
985
            'category' => Category::CATEGORY_ENGINEERING,
986
            'functionCall' => [Engineering::class, 'HEXTOBIN'],
987
            'argumentCount' => '1,2',
988
        ],
989
        'HEX2DEC' => [
990
            'category' => Category::CATEGORY_ENGINEERING,
991
            'functionCall' => [Engineering::class, 'HEXTODEC'],
992
            'argumentCount' => '1',
993
        ],
994
        'HEX2OCT' => [
995
            'category' => Category::CATEGORY_ENGINEERING,
996
            'functionCall' => [Engineering::class, 'HEXTOOCT'],
997
            'argumentCount' => '1,2',
998
        ],
999
        'HLOOKUP' => [
1000
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1001
            'functionCall' => [LookupRef::class, 'HLOOKUP'],
1002
            'argumentCount' => '3,4',
1003
        ],
1004
        'HOUR' => [
1005
            'category' => Category::CATEGORY_DATE_AND_TIME,
1006
            'functionCall' => [DateTime::class, 'HOUROFDAY'],
1007
            'argumentCount' => '1',
1008
        ],
1009
        'HYPERLINK' => [
1010
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1011
            'functionCall' => [LookupRef::class, 'HYPERLINK'],
1012
            'argumentCount' => '1,2',
1013
            'passCellReference' => true,
1014
        ],
1015
        'HYPGEOMDIST' => [
1016
            'category' => Category::CATEGORY_STATISTICAL,
1017
            'functionCall' => [Statistical::class, 'HYPGEOMDIST'],
1018
            'argumentCount' => '4',
1019
        ],
1020
        'IF' => [
1021
            'category' => Category::CATEGORY_LOGICAL,
1022
            'functionCall' => [Logical::class, 'statementIf'],
1023
            'argumentCount' => '1-3',
1024
        ],
1025
        'IFERROR' => [
1026
            'category' => Category::CATEGORY_LOGICAL,
1027
            'functionCall' => [Logical::class, 'IFERROR'],
1028
            'argumentCount' => '2',
1029
        ],
1030
        'IFNA' => [
1031
            'category' => Category::CATEGORY_LOGICAL,
1032
            'functionCall' => [Logical::class, 'IFNA'],
1033
            'argumentCount' => '2',
1034
        ],
1035
        'IMABS' => [
1036
            'category' => Category::CATEGORY_ENGINEERING,
1037
            'functionCall' => [Engineering::class, 'IMABS'],
1038
            'argumentCount' => '1',
1039
        ],
1040
        'IMAGINARY' => [
1041
            'category' => Category::CATEGORY_ENGINEERING,
1042
            'functionCall' => [Engineering::class, 'IMAGINARY'],
1043
            'argumentCount' => '1',
1044
        ],
1045
        'IMARGUMENT' => [
1046
            'category' => Category::CATEGORY_ENGINEERING,
1047
            'functionCall' => [Engineering::class, 'IMARGUMENT'],
1048
            'argumentCount' => '1',
1049
        ],
1050
        'IMCONJUGATE' => [
1051
            'category' => Category::CATEGORY_ENGINEERING,
1052
            'functionCall' => [Engineering::class, 'IMCONJUGATE'],
1053
            'argumentCount' => '1',
1054
        ],
1055
        'IMCOS' => [
1056
            'category' => Category::CATEGORY_ENGINEERING,
1057
            'functionCall' => [Engineering::class, 'IMCOS'],
1058
            'argumentCount' => '1',
1059
        ],
1060
        'IMCOSH' => [
1061
            'category' => Category::CATEGORY_ENGINEERING,
1062
            'functionCall' => [Engineering::class, 'IMCOSH'],
1063
            'argumentCount' => '1',
1064
        ],
1065
        'IMCOT' => [
1066
            'category' => Category::CATEGORY_ENGINEERING,
1067
            'functionCall' => [Engineering::class, 'IMCOT'],
1068
            'argumentCount' => '1',
1069
        ],
1070
        'IMCSC' => [
1071
            'category' => Category::CATEGORY_ENGINEERING,
1072
            'functionCall' => [Engineering::class, 'IMCSC'],
1073
            'argumentCount' => '1',
1074
        ],
1075
        'IMCSCH' => [
1076
            'category' => Category::CATEGORY_ENGINEERING,
1077
            'functionCall' => [Engineering::class, 'IMCSCH'],
1078
            'argumentCount' => '1',
1079
        ],
1080
        'IMDIV' => [
1081
            'category' => Category::CATEGORY_ENGINEERING,
1082
            'functionCall' => [Engineering::class, 'IMDIV'],
1083
            'argumentCount' => '2',
1084
        ],
1085
        'IMEXP' => [
1086
            'category' => Category::CATEGORY_ENGINEERING,
1087
            'functionCall' => [Engineering::class, 'IMEXP'],
1088
            'argumentCount' => '1',
1089
        ],
1090
        'IMLN' => [
1091
            'category' => Category::CATEGORY_ENGINEERING,
1092
            'functionCall' => [Engineering::class, 'IMLN'],
1093
            'argumentCount' => '1',
1094
        ],
1095
        'IMLOG10' => [
1096
            'category' => Category::CATEGORY_ENGINEERING,
1097
            'functionCall' => [Engineering::class, 'IMLOG10'],
1098
            'argumentCount' => '1',
1099
        ],
1100
        'IMLOG2' => [
1101
            'category' => Category::CATEGORY_ENGINEERING,
1102
            'functionCall' => [Engineering::class, 'IMLOG2'],
1103
            'argumentCount' => '1',
1104
        ],
1105
        'IMPOWER' => [
1106
            'category' => Category::CATEGORY_ENGINEERING,
1107
            'functionCall' => [Engineering::class, 'IMPOWER'],
1108
            'argumentCount' => '2',
1109
        ],
1110
        'IMPRODUCT' => [
1111
            'category' => Category::CATEGORY_ENGINEERING,
1112
            'functionCall' => [Engineering::class, 'IMPRODUCT'],
1113
            'argumentCount' => '1+',
1114
        ],
1115
        'IMREAL' => [
1116
            'category' => Category::CATEGORY_ENGINEERING,
1117
            'functionCall' => [Engineering::class, 'IMREAL'],
1118
            'argumentCount' => '1',
1119
        ],
1120
        'IMSEC' => [
1121
            'category' => Category::CATEGORY_ENGINEERING,
1122
            'functionCall' => [Engineering::class, 'IMSEC'],
1123
            'argumentCount' => '1',
1124
        ],
1125
        'IMSECH' => [
1126
            'category' => Category::CATEGORY_ENGINEERING,
1127
            'functionCall' => [Engineering::class, 'IMSECH'],
1128
            'argumentCount' => '1',
1129
        ],
1130
        'IMSIN' => [
1131
            'category' => Category::CATEGORY_ENGINEERING,
1132
            'functionCall' => [Engineering::class, 'IMSIN'],
1133
            'argumentCount' => '1',
1134
        ],
1135
        'IMSINH' => [
1136
            'category' => Category::CATEGORY_ENGINEERING,
1137
            'functionCall' => [Engineering::class, 'IMSINH'],
1138
            'argumentCount' => '1',
1139
        ],
1140
        'IMSQRT' => [
1141
            'category' => Category::CATEGORY_ENGINEERING,
1142
            'functionCall' => [Engineering::class, 'IMSQRT'],
1143
            'argumentCount' => '1',
1144
        ],
1145
        'IMSUB' => [
1146
            'category' => Category::CATEGORY_ENGINEERING,
1147
            'functionCall' => [Engineering::class, 'IMSUB'],
1148
            'argumentCount' => '2',
1149
        ],
1150
        'IMSUM' => [
1151
            'category' => Category::CATEGORY_ENGINEERING,
1152
            'functionCall' => [Engineering::class, 'IMSUM'],
1153
            'argumentCount' => '1+',
1154
        ],
1155
        'IMTAN' => [
1156
            'category' => Category::CATEGORY_ENGINEERING,
1157
            'functionCall' => [Engineering::class, 'IMTAN'],
1158
            'argumentCount' => '1',
1159
        ],
1160
        'INDEX' => [
1161
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1162
            'functionCall' => [LookupRef::class, 'INDEX'],
1163
            'argumentCount' => '1-4',
1164
        ],
1165
        'INDIRECT' => [
1166
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1167
            'functionCall' => [LookupRef::class, 'INDIRECT'],
1168
            'argumentCount' => '1,2',
1169
            'passCellReference' => true,
1170
        ],
1171
        'INFO' => [
1172
            'category' => Category::CATEGORY_INFORMATION,
1173
            'functionCall' => [Functions::class, 'DUMMY'],
1174
            'argumentCount' => '1',
1175
        ],
1176
        'INT' => [
1177
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1178
            'functionCall' => [MathTrig::class, 'INT'],
1179
            'argumentCount' => '1',
1180
        ],
1181
        'INTERCEPT' => [
1182
            'category' => Category::CATEGORY_STATISTICAL,
1183
            'functionCall' => [Statistical::class, 'INTERCEPT'],
1184
            'argumentCount' => '2',
1185
        ],
1186
        'INTRATE' => [
1187
            'category' => Category::CATEGORY_FINANCIAL,
1188
            'functionCall' => [Financial::class, 'INTRATE'],
1189
            'argumentCount' => '4,5',
1190
        ],
1191
        'IPMT' => [
1192
            'category' => Category::CATEGORY_FINANCIAL,
1193
            'functionCall' => [Financial::class, 'IPMT'],
1194
            'argumentCount' => '4-6',
1195
        ],
1196
        'IRR' => [
1197
            'category' => Category::CATEGORY_FINANCIAL,
1198
            'functionCall' => [Financial::class, 'IRR'],
1199
            'argumentCount' => '1,2',
1200
        ],
1201
        'ISBLANK' => [
1202
            'category' => Category::CATEGORY_INFORMATION,
1203
            'functionCall' => [Functions::class, 'isBlank'],
1204
            'argumentCount' => '1',
1205
        ],
1206
        'ISERR' => [
1207
            'category' => Category::CATEGORY_INFORMATION,
1208
            'functionCall' => [Functions::class, 'isErr'],
1209
            'argumentCount' => '1',
1210
        ],
1211
        'ISERROR' => [
1212
            'category' => Category::CATEGORY_INFORMATION,
1213
            'functionCall' => [Functions::class, 'isError'],
1214
            'argumentCount' => '1',
1215
        ],
1216
        'ISEVEN' => [
1217
            'category' => Category::CATEGORY_INFORMATION,
1218
            'functionCall' => [Functions::class, 'isEven'],
1219
            'argumentCount' => '1',
1220
        ],
1221
        'ISFORMULA' => [
1222
            'category' => Category::CATEGORY_INFORMATION,
1223
            'functionCall' => [Functions::class, 'isFormula'],
1224
            'argumentCount' => '1',
1225
            'passCellReference' => true,
1226
            'passByReference' => [true],
1227
        ],
1228
        'ISLOGICAL' => [
1229
            'category' => Category::CATEGORY_INFORMATION,
1230
            'functionCall' => [Functions::class, 'isLogical'],
1231
            'argumentCount' => '1',
1232
        ],
1233
        'ISNA' => [
1234
            'category' => Category::CATEGORY_INFORMATION,
1235
            'functionCall' => [Functions::class, 'isNa'],
1236
            'argumentCount' => '1',
1237
        ],
1238
        'ISNONTEXT' => [
1239
            'category' => Category::CATEGORY_INFORMATION,
1240
            'functionCall' => [Functions::class, 'isNonText'],
1241
            'argumentCount' => '1',
1242
        ],
1243
        'ISNUMBER' => [
1244
            'category' => Category::CATEGORY_INFORMATION,
1245
            'functionCall' => [Functions::class, 'isNumber'],
1246
            'argumentCount' => '1',
1247
        ],
1248
        'ISODD' => [
1249
            'category' => Category::CATEGORY_INFORMATION,
1250
            'functionCall' => [Functions::class, 'isOdd'],
1251
            'argumentCount' => '1',
1252
        ],
1253
        'ISOWEEKNUM' => [
1254
            'category' => Category::CATEGORY_DATE_AND_TIME,
1255
            'functionCall' => [DateTime::class, 'ISOWEEKNUM'],
1256
            'argumentCount' => '1',
1257
        ],
1258
        'ISPMT' => [
1259
            'category' => Category::CATEGORY_FINANCIAL,
1260
            'functionCall' => [Financial::class, 'ISPMT'],
1261
            'argumentCount' => '4',
1262
        ],
1263
        'ISREF' => [
1264
            'category' => Category::CATEGORY_INFORMATION,
1265
            'functionCall' => [Functions::class, 'DUMMY'],
1266
            'argumentCount' => '1',
1267
        ],
1268
        'ISTEXT' => [
1269
            'category' => Category::CATEGORY_INFORMATION,
1270
            'functionCall' => [Functions::class, 'isText'],
1271
            'argumentCount' => '1',
1272
        ],
1273
        'JIS' => [
1274
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1275
            'functionCall' => [Functions::class, 'DUMMY'],
1276
            'argumentCount' => '1',
1277
        ],
1278
        'KURT' => [
1279
            'category' => Category::CATEGORY_STATISTICAL,
1280
            'functionCall' => [Statistical::class, 'KURT'],
1281
            'argumentCount' => '1+',
1282
        ],
1283
        'LARGE' => [
1284
            'category' => Category::CATEGORY_STATISTICAL,
1285
            'functionCall' => [Statistical::class, 'LARGE'],
1286
            'argumentCount' => '2',
1287
        ],
1288
        'LCM' => [
1289
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1290
            'functionCall' => [MathTrig::class, 'LCM'],
1291
            'argumentCount' => '1+',
1292
        ],
1293
        'LEFT' => [
1294
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1295
            'functionCall' => [TextData::class, 'LEFT'],
1296
            'argumentCount' => '1,2',
1297
        ],
1298
        'LEFTB' => [
1299
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1300
            'functionCall' => [TextData::class, 'LEFT'],
1301
            'argumentCount' => '1,2',
1302
        ],
1303
        'LEN' => [
1304
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1305
            'functionCall' => [TextData::class, 'STRINGLENGTH'],
1306
            'argumentCount' => '1',
1307
        ],
1308
        'LENB' => [
1309
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1310
            'functionCall' => [TextData::class, 'STRINGLENGTH'],
1311
            'argumentCount' => '1',
1312
        ],
1313
        'LINEST' => [
1314
            'category' => Category::CATEGORY_STATISTICAL,
1315
            'functionCall' => [Statistical::class, 'LINEST'],
1316
            'argumentCount' => '1-4',
1317
        ],
1318
        'LN' => [
1319
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1320
            'functionCall' => 'log',
1321
            'argumentCount' => '1',
1322
        ],
1323
        'LOG' => [
1324
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1325
            'functionCall' => [MathTrig::class, 'logBase'],
1326
            'argumentCount' => '1,2',
1327
        ],
1328
        'LOG10' => [
1329
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1330
            'functionCall' => 'log10',
1331
            'argumentCount' => '1',
1332
        ],
1333
        'LOGEST' => [
1334
            'category' => Category::CATEGORY_STATISTICAL,
1335
            'functionCall' => [Statistical::class, 'LOGEST'],
1336
            'argumentCount' => '1-4',
1337
        ],
1338
        'LOGINV' => [
1339
            'category' => Category::CATEGORY_STATISTICAL,
1340
            'functionCall' => [Statistical::class, 'LOGINV'],
1341
            'argumentCount' => '3',
1342
        ],
1343
        'LOGNORMDIST' => [
1344
            'category' => Category::CATEGORY_STATISTICAL,
1345
            'functionCall' => [Statistical::class, 'LOGNORMDIST'],
1346
            'argumentCount' => '3',
1347
        ],
1348
        'LOOKUP' => [
1349
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1350
            'functionCall' => [LookupRef::class, 'LOOKUP'],
1351
            'argumentCount' => '2,3',
1352
        ],
1353
        'LOWER' => [
1354
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1355
            'functionCall' => [TextData::class, 'LOWERCASE'],
1356
            'argumentCount' => '1',
1357
        ],
1358
        'MATCH' => [
1359
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1360
            'functionCall' => [LookupRef::class, 'MATCH'],
1361
            'argumentCount' => '2,3',
1362
        ],
1363
        'MAX' => [
1364
            'category' => Category::CATEGORY_STATISTICAL,
1365
            'functionCall' => [Statistical::class, 'MAX'],
1366
            'argumentCount' => '1+',
1367
        ],
1368
        'MAXA' => [
1369
            'category' => Category::CATEGORY_STATISTICAL,
1370
            'functionCall' => [Statistical::class, 'MAXA'],
1371
            'argumentCount' => '1+',
1372
        ],
1373
        'MAXIFS' => [
1374
            'category' => Category::CATEGORY_STATISTICAL,
1375
            'functionCall' => [Statistical::class, 'MAXIFS'],
1376
            'argumentCount' => '3+',
1377
        ],
1378
        'MDETERM' => [
1379
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1380
            'functionCall' => [MathTrig::class, 'MDETERM'],
1381
            'argumentCount' => '1',
1382
        ],
1383
        'MDURATION' => [
1384
            'category' => Category::CATEGORY_FINANCIAL,
1385
            'functionCall' => [Functions::class, 'DUMMY'],
1386
            'argumentCount' => '5,6',
1387
        ],
1388
        'MEDIAN' => [
1389
            'category' => Category::CATEGORY_STATISTICAL,
1390
            'functionCall' => [Statistical::class, 'MEDIAN'],
1391
            'argumentCount' => '1+',
1392
        ],
1393
        'MEDIANIF' => [
1394
            'category' => Category::CATEGORY_STATISTICAL,
1395
            'functionCall' => [Functions::class, 'DUMMY'],
1396
            'argumentCount' => '2+',
1397
        ],
1398
        'MID' => [
1399
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1400
            'functionCall' => [TextData::class, 'MID'],
1401
            'argumentCount' => '3',
1402
        ],
1403
        'MIDB' => [
1404
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1405
            'functionCall' => [TextData::class, 'MID'],
1406
            'argumentCount' => '3',
1407
        ],
1408
        'MIN' => [
1409
            'category' => Category::CATEGORY_STATISTICAL,
1410
            'functionCall' => [Statistical::class, 'MIN'],
1411
            'argumentCount' => '1+',
1412
        ],
1413
        'MINA' => [
1414
            'category' => Category::CATEGORY_STATISTICAL,
1415
            'functionCall' => [Statistical::class, 'MINA'],
1416
            'argumentCount' => '1+',
1417
        ],
1418
        'MINIFS' => [
1419
            'category' => Category::CATEGORY_STATISTICAL,
1420
            'functionCall' => [Statistical::class, 'MINIFS'],
1421
            'argumentCount' => '3+',
1422
        ],
1423
        'MINUTE' => [
1424
            'category' => Category::CATEGORY_DATE_AND_TIME,
1425
            'functionCall' => [DateTime::class, 'MINUTE'],
1426
            'argumentCount' => '1',
1427
        ],
1428
        'MINVERSE' => [
1429
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1430
            'functionCall' => [MathTrig::class, 'MINVERSE'],
1431
            'argumentCount' => '1',
1432
        ],
1433
        'MIRR' => [
1434
            'category' => Category::CATEGORY_FINANCIAL,
1435
            'functionCall' => [Financial::class, 'MIRR'],
1436
            'argumentCount' => '3',
1437
        ],
1438
        'MMULT' => [
1439
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1440
            'functionCall' => [MathTrig::class, 'MMULT'],
1441
            'argumentCount' => '2',
1442
        ],
1443
        'MOD' => [
1444
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1445
            'functionCall' => [MathTrig::class, 'MOD'],
1446
            'argumentCount' => '2',
1447
        ],
1448
        'MODE' => [
1449
            'category' => Category::CATEGORY_STATISTICAL,
1450
            'functionCall' => [Statistical::class, 'MODE'],
1451
            'argumentCount' => '1+',
1452
        ],
1453
        'MODE.SNGL' => [
1454
            'category' => Category::CATEGORY_STATISTICAL,
1455
            'functionCall' => [Statistical::class, 'MODE'],
1456
            'argumentCount' => '1+',
1457
        ],
1458
        'MONTH' => [
1459
            'category' => Category::CATEGORY_DATE_AND_TIME,
1460
            'functionCall' => [DateTime::class, 'MONTHOFYEAR'],
1461
            'argumentCount' => '1',
1462
        ],
1463
        'MROUND' => [
1464
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1465
            'functionCall' => [MathTrig::class, 'MROUND'],
1466
            'argumentCount' => '2',
1467
        ],
1468
        'MULTINOMIAL' => [
1469
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1470
            'functionCall' => [MathTrig::class, 'MULTINOMIAL'],
1471
            'argumentCount' => '1+',
1472
        ],
1473
        'N' => [
1474
            'category' => Category::CATEGORY_INFORMATION,
1475
            'functionCall' => [Functions::class, 'n'],
1476
            'argumentCount' => '1',
1477
        ],
1478
        'NA' => [
1479
            'category' => Category::CATEGORY_INFORMATION,
1480
            'functionCall' => [Functions::class, 'NA'],
1481
            'argumentCount' => '0',
1482
        ],
1483
        'NEGBINOMDIST' => [
1484
            'category' => Category::CATEGORY_STATISTICAL,
1485
            'functionCall' => [Statistical::class, 'NEGBINOMDIST'],
1486
            'argumentCount' => '3',
1487
        ],
1488
        'NETWORKDAYS' => [
1489
            'category' => Category::CATEGORY_DATE_AND_TIME,
1490
            'functionCall' => [DateTime::class, 'NETWORKDAYS'],
1491
            'argumentCount' => '2+',
1492
        ],
1493
        'NOMINAL' => [
1494
            'category' => Category::CATEGORY_FINANCIAL,
1495
            'functionCall' => [Financial::class, 'NOMINAL'],
1496
            'argumentCount' => '2',
1497
        ],
1498
        'NORMDIST' => [
1499
            'category' => Category::CATEGORY_STATISTICAL,
1500
            'functionCall' => [Statistical::class, 'NORMDIST'],
1501
            'argumentCount' => '4',
1502
        ],
1503
        'NORMINV' => [
1504
            'category' => Category::CATEGORY_STATISTICAL,
1505
            'functionCall' => [Statistical::class, 'NORMINV'],
1506
            'argumentCount' => '3',
1507
        ],
1508
        'NORMSDIST' => [
1509
            'category' => Category::CATEGORY_STATISTICAL,
1510
            'functionCall' => [Statistical::class, 'NORMSDIST'],
1511
            'argumentCount' => '1',
1512
        ],
1513
        'NORMSINV' => [
1514
            'category' => Category::CATEGORY_STATISTICAL,
1515
            'functionCall' => [Statistical::class, 'NORMSINV'],
1516
            'argumentCount' => '1',
1517
        ],
1518
        'NOT' => [
1519
            'category' => Category::CATEGORY_LOGICAL,
1520
            'functionCall' => [Logical::class, 'NOT'],
1521
            'argumentCount' => '1',
1522
        ],
1523
        'NOW' => [
1524
            'category' => Category::CATEGORY_DATE_AND_TIME,
1525
            'functionCall' => [DateTime::class, 'DATETIMENOW'],
1526
            'argumentCount' => '0',
1527
        ],
1528
        'NPER' => [
1529
            'category' => Category::CATEGORY_FINANCIAL,
1530
            'functionCall' => [Financial::class, 'NPER'],
1531
            'argumentCount' => '3-5',
1532
        ],
1533
        'NPV' => [
1534
            'category' => Category::CATEGORY_FINANCIAL,
1535
            'functionCall' => [Financial::class, 'NPV'],
1536
            'argumentCount' => '2+',
1537
        ],
1538
        'NUMBERVALUE' => [
1539
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1540
            'functionCall' => [TextData::class, 'NUMBERVALUE'],
1541
            'argumentCount' => '1+',
1542
        ],
1543
        'OCT2BIN' => [
1544
            'category' => Category::CATEGORY_ENGINEERING,
1545
            'functionCall' => [Engineering::class, 'OCTTOBIN'],
1546
            'argumentCount' => '1,2',
1547
        ],
1548
        'OCT2DEC' => [
1549
            'category' => Category::CATEGORY_ENGINEERING,
1550
            'functionCall' => [Engineering::class, 'OCTTODEC'],
1551
            'argumentCount' => '1',
1552
        ],
1553
        'OCT2HEX' => [
1554
            'category' => Category::CATEGORY_ENGINEERING,
1555
            'functionCall' => [Engineering::class, 'OCTTOHEX'],
1556
            'argumentCount' => '1,2',
1557
        ],
1558
        'ODD' => [
1559
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1560
            'functionCall' => [MathTrig::class, 'ODD'],
1561
            'argumentCount' => '1',
1562
        ],
1563
        'ODDFPRICE' => [
1564
            'category' => Category::CATEGORY_FINANCIAL,
1565
            'functionCall' => [Functions::class, 'DUMMY'],
1566
            'argumentCount' => '8,9',
1567
        ],
1568
        'ODDFYIELD' => [
1569
            'category' => Category::CATEGORY_FINANCIAL,
1570
            'functionCall' => [Functions::class, 'DUMMY'],
1571
            'argumentCount' => '8,9',
1572
        ],
1573
        'ODDLPRICE' => [
1574
            'category' => Category::CATEGORY_FINANCIAL,
1575
            'functionCall' => [Functions::class, 'DUMMY'],
1576
            'argumentCount' => '7,8',
1577
        ],
1578
        'ODDLYIELD' => [
1579
            'category' => Category::CATEGORY_FINANCIAL,
1580
            'functionCall' => [Functions::class, 'DUMMY'],
1581
            'argumentCount' => '7,8',
1582
        ],
1583
        'OFFSET' => [
1584
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1585
            'functionCall' => [LookupRef::class, 'OFFSET'],
1586
            'argumentCount' => '3-5',
1587
            'passCellReference' => true,
1588
            'passByReference' => [true],
1589
        ],
1590
        'OR' => [
1591
            'category' => Category::CATEGORY_LOGICAL,
1592
            'functionCall' => [Logical::class, 'logicalOr'],
1593
            'argumentCount' => '1+',
1594
        ],
1595
        'PDURATION' => [
1596
            'category' => Category::CATEGORY_FINANCIAL,
1597
            'functionCall' => [Financial::class, 'PDURATION'],
1598
            'argumentCount' => '3',
1599
        ],
1600
        'PEARSON' => [
1601
            'category' => Category::CATEGORY_STATISTICAL,
1602
            'functionCall' => [Statistical::class, 'CORREL'],
1603
            'argumentCount' => '2',
1604
        ],
1605
        'PERCENTILE' => [
1606
            'category' => Category::CATEGORY_STATISTICAL,
1607
            'functionCall' => [Statistical::class, 'PERCENTILE'],
1608
            'argumentCount' => '2',
1609
        ],
1610
        'PERCENTRANK' => [
1611
            'category' => Category::CATEGORY_STATISTICAL,
1612
            'functionCall' => [Statistical::class, 'PERCENTRANK'],
1613
            'argumentCount' => '2,3',
1614
        ],
1615
        'PERMUT' => [
1616
            'category' => Category::CATEGORY_STATISTICAL,
1617
            'functionCall' => [Statistical::class, 'PERMUT'],
1618
            'argumentCount' => '2',
1619
        ],
1620
        'PHONETIC' => [
1621
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1622
            'functionCall' => [Functions::class, 'DUMMY'],
1623
            'argumentCount' => '1',
1624
        ],
1625
        'PI' => [
1626
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1627
            'functionCall' => 'pi',
1628
            'argumentCount' => '0',
1629
        ],
1630
        'PMT' => [
1631
            'category' => Category::CATEGORY_FINANCIAL,
1632
            'functionCall' => [Financial::class, 'PMT'],
1633
            'argumentCount' => '3-5',
1634
        ],
1635
        'POISSON' => [
1636
            'category' => Category::CATEGORY_STATISTICAL,
1637
            'functionCall' => [Statistical::class, 'POISSON'],
1638
            'argumentCount' => '3',
1639
        ],
1640
        'POWER' => [
1641
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1642
            'functionCall' => [MathTrig::class, 'POWER'],
1643
            'argumentCount' => '2',
1644
        ],
1645
        'PPMT' => [
1646
            'category' => Category::CATEGORY_FINANCIAL,
1647
            'functionCall' => [Financial::class, 'PPMT'],
1648
            'argumentCount' => '4-6',
1649
        ],
1650
        'PRICE' => [
1651
            'category' => Category::CATEGORY_FINANCIAL,
1652
            'functionCall' => [Financial::class, 'PRICE'],
1653
            'argumentCount' => '6,7',
1654
        ],
1655
        'PRICEDISC' => [
1656
            'category' => Category::CATEGORY_FINANCIAL,
1657
            'functionCall' => [Financial::class, 'PRICEDISC'],
1658
            'argumentCount' => '4,5',
1659
        ],
1660
        'PRICEMAT' => [
1661
            'category' => Category::CATEGORY_FINANCIAL,
1662
            'functionCall' => [Financial::class, 'PRICEMAT'],
1663
            'argumentCount' => '5,6',
1664
        ],
1665
        'PROB' => [
1666
            'category' => Category::CATEGORY_STATISTICAL,
1667
            'functionCall' => [Functions::class, 'DUMMY'],
1668
            'argumentCount' => '3,4',
1669
        ],
1670
        'PRODUCT' => [
1671
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1672
            'functionCall' => [MathTrig::class, 'PRODUCT'],
1673
            'argumentCount' => '1+',
1674
        ],
1675
        'PROPER' => [
1676
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1677
            'functionCall' => [TextData::class, 'PROPERCASE'],
1678
            'argumentCount' => '1',
1679
        ],
1680
        'PV' => [
1681
            'category' => Category::CATEGORY_FINANCIAL,
1682
            'functionCall' => [Financial::class, 'PV'],
1683
            'argumentCount' => '3-5',
1684
        ],
1685
        'QUARTILE' => [
1686
            'category' => Category::CATEGORY_STATISTICAL,
1687
            'functionCall' => [Statistical::class, 'QUARTILE'],
1688
            'argumentCount' => '2',
1689
        ],
1690
        'QUOTIENT' => [
1691
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1692
            'functionCall' => [MathTrig::class, 'QUOTIENT'],
1693
            'argumentCount' => '2',
1694
        ],
1695
        'RADIANS' => [
1696
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1697
            'functionCall' => 'deg2rad',
1698
            'argumentCount' => '1',
1699
        ],
1700
        'RAND' => [
1701
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1702
            'functionCall' => [MathTrig::class, 'RAND'],
1703
            'argumentCount' => '0',
1704
        ],
1705
        'RANDBETWEEN' => [
1706
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1707
            'functionCall' => [MathTrig::class, 'RAND'],
1708
            'argumentCount' => '2',
1709
        ],
1710
        'RANK' => [
1711
            'category' => Category::CATEGORY_STATISTICAL,
1712
            'functionCall' => [Statistical::class, 'RANK'],
1713
            'argumentCount' => '2,3',
1714
        ],
1715
        'RATE' => [
1716
            'category' => Category::CATEGORY_FINANCIAL,
1717
            'functionCall' => [Financial::class, 'RATE'],
1718
            'argumentCount' => '3-6',
1719
        ],
1720
        'RECEIVED' => [
1721
            'category' => Category::CATEGORY_FINANCIAL,
1722
            'functionCall' => [Financial::class, 'RECEIVED'],
1723
            'argumentCount' => '4-5',
1724
        ],
1725
        'REPLACE' => [
1726
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1727
            'functionCall' => [TextData::class, 'REPLACE'],
1728
            'argumentCount' => '4',
1729
        ],
1730
        'REPLACEB' => [
1731
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1732
            'functionCall' => [TextData::class, 'REPLACE'],
1733
            'argumentCount' => '4',
1734
        ],
1735
        'REPT' => [
1736
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1737
            'functionCall' => 'str_repeat',
1738
            'argumentCount' => '2',
1739
        ],
1740
        'RIGHT' => [
1741
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1742
            'functionCall' => [TextData::class, 'RIGHT'],
1743
            'argumentCount' => '1,2',
1744
        ],
1745
        'RIGHTB' => [
1746
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1747
            'functionCall' => [TextData::class, 'RIGHT'],
1748
            'argumentCount' => '1,2',
1749
        ],
1750
        'ROMAN' => [
1751
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1752
            'functionCall' => [MathTrig::class, 'ROMAN'],
1753
            'argumentCount' => '1,2',
1754
        ],
1755
        'ROUND' => [
1756
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1757
            'functionCall' => 'round',
1758
            'argumentCount' => '2',
1759
        ],
1760
        'ROUNDDOWN' => [
1761
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1762
            'functionCall' => [MathTrig::class, 'ROUNDDOWN'],
1763
            'argumentCount' => '2',
1764
        ],
1765
        'ROUNDUP' => [
1766
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1767
            'functionCall' => [MathTrig::class, 'ROUNDUP'],
1768
            'argumentCount' => '2',
1769
        ],
1770
        'ROW' => [
1771
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1772
            'functionCall' => [LookupRef::class, 'ROW'],
1773
            'argumentCount' => '-1',
1774
            'passByReference' => [true],
1775
        ],
1776
        'ROWS' => [
1777
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1778
            'functionCall' => [LookupRef::class, 'ROWS'],
1779
            'argumentCount' => '1',
1780
        ],
1781
        'RRI' => [
1782
            'category' => Category::CATEGORY_FINANCIAL,
1783
            'functionCall' => [Financial::class, 'RRI'],
1784
            'argumentCount' => '3',
1785
        ],
1786
        'RSQ' => [
1787
            'category' => Category::CATEGORY_STATISTICAL,
1788
            'functionCall' => [Statistical::class, 'RSQ'],
1789
            'argumentCount' => '2',
1790
        ],
1791
        'RTD' => [
1792
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1793
            'functionCall' => [Functions::class, 'DUMMY'],
1794
            'argumentCount' => '1+',
1795
        ],
1796
        'SEARCH' => [
1797
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1798
            'functionCall' => [TextData::class, 'SEARCHINSENSITIVE'],
1799
            'argumentCount' => '2,3',
1800
        ],
1801
        'SEARCHB' => [
1802
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1803
            'functionCall' => [TextData::class, 'SEARCHINSENSITIVE'],
1804
            'argumentCount' => '2,3',
1805
        ],
1806
        'SEC' => [
1807
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1808
            'functionCall' => [MathTrig::class, 'SEC'],
1809
            'argumentCount' => '1',
1810
        ],
1811
        'SECH' => [
1812
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1813
            'functionCall' => [MathTrig::class, 'SECH'],
1814
            'argumentCount' => '1',
1815
        ],
1816
        'SECOND' => [
1817
            'category' => Category::CATEGORY_DATE_AND_TIME,
1818
            'functionCall' => [DateTime::class, 'SECOND'],
1819
            'argumentCount' => '1',
1820
        ],
1821
        'SERIESSUM' => [
1822
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1823
            'functionCall' => [MathTrig::class, 'SERIESSUM'],
1824
            'argumentCount' => '4',
1825
        ],
1826
        'SIGN' => [
1827
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1828
            'functionCall' => [MathTrig::class, 'SIGN'],
1829
            'argumentCount' => '1',
1830
        ],
1831
        'SIN' => [
1832
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1833
            'functionCall' => 'sin',
1834
            'argumentCount' => '1',
1835
        ],
1836
        'SINH' => [
1837
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1838
            'functionCall' => 'sinh',
1839
            'argumentCount' => '1',
1840
        ],
1841
        'SKEW' => [
1842
            'category' => Category::CATEGORY_STATISTICAL,
1843
            'functionCall' => [Statistical::class, 'SKEW'],
1844
            'argumentCount' => '1+',
1845
        ],
1846
        'SLN' => [
1847
            'category' => Category::CATEGORY_FINANCIAL,
1848
            'functionCall' => [Financial::class, 'SLN'],
1849
            'argumentCount' => '3',
1850
        ],
1851
        'SLOPE' => [
1852
            'category' => Category::CATEGORY_STATISTICAL,
1853
            'functionCall' => [Statistical::class, 'SLOPE'],
1854
            'argumentCount' => '2',
1855
        ],
1856
        'SMALL' => [
1857
            'category' => Category::CATEGORY_STATISTICAL,
1858
            'functionCall' => [Statistical::class, 'SMALL'],
1859
            'argumentCount' => '2',
1860
        ],
1861
        'SQRT' => [
1862
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1863
            'functionCall' => 'sqrt',
1864
            'argumentCount' => '1',
1865
        ],
1866
        'SQRTPI' => [
1867
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1868
            'functionCall' => [MathTrig::class, 'SQRTPI'],
1869
            'argumentCount' => '1',
1870
        ],
1871
        'STANDARDIZE' => [
1872
            'category' => Category::CATEGORY_STATISTICAL,
1873
            'functionCall' => [Statistical::class, 'STANDARDIZE'],
1874
            'argumentCount' => '3',
1875
        ],
1876
        'STDEV' => [
1877
            'category' => Category::CATEGORY_STATISTICAL,
1878
            'functionCall' => [Statistical::class, 'STDEV'],
1879
            'argumentCount' => '1+',
1880
        ],
1881
        'STDEV.S' => [
1882
            'category' => Category::CATEGORY_STATISTICAL,
1883
            'functionCall' => [Statistical::class, 'STDEV'],
1884
            'argumentCount' => '1+',
1885
        ],
1886
        'STDEV.P' => [
1887
            'category' => Category::CATEGORY_STATISTICAL,
1888
            'functionCall' => [Statistical::class, 'STDEVP'],
1889
            'argumentCount' => '1+',
1890
        ],
1891
        'STDEVA' => [
1892
            'category' => Category::CATEGORY_STATISTICAL,
1893
            'functionCall' => [Statistical::class, 'STDEVA'],
1894
            'argumentCount' => '1+',
1895
        ],
1896
        'STDEVP' => [
1897
            'category' => Category::CATEGORY_STATISTICAL,
1898
            'functionCall' => [Statistical::class, 'STDEVP'],
1899
            'argumentCount' => '1+',
1900
        ],
1901
        'STDEVPA' => [
1902
            'category' => Category::CATEGORY_STATISTICAL,
1903
            'functionCall' => [Statistical::class, 'STDEVPA'],
1904
            'argumentCount' => '1+',
1905
        ],
1906
        'STEYX' => [
1907
            'category' => Category::CATEGORY_STATISTICAL,
1908
            'functionCall' => [Statistical::class, 'STEYX'],
1909
            'argumentCount' => '2',
1910
        ],
1911
        'SUBSTITUTE' => [
1912
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1913
            'functionCall' => [TextData::class, 'SUBSTITUTE'],
1914
            'argumentCount' => '3,4',
1915
        ],
1916
        'SUBTOTAL' => [
1917
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1918
            'functionCall' => [MathTrig::class, 'SUBTOTAL'],
1919
            'argumentCount' => '2+',
1920
            'passCellReference' => true,
1921
        ],
1922
        'SUM' => [
1923
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1924
            'functionCall' => [MathTrig::class, 'SUM'],
1925
            'argumentCount' => '1+',
1926
        ],
1927
        'SUMIF' => [
1928
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1929
            'functionCall' => [MathTrig::class, 'SUMIF'],
1930
            'argumentCount' => '2,3',
1931
        ],
1932
        'SUMIFS' => [
1933
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1934
            'functionCall' => [MathTrig::class, 'SUMIFS'],
1935
            'argumentCount' => '3+',
1936
        ],
1937
        'SUMPRODUCT' => [
1938
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1939
            'functionCall' => [MathTrig::class, 'SUMPRODUCT'],
1940
            'argumentCount' => '1+',
1941
        ],
1942
        'SUMSQ' => [
1943
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1944
            'functionCall' => [MathTrig::class, 'SUMSQ'],
1945
            'argumentCount' => '1+',
1946
        ],
1947
        'SUMX2MY2' => [
1948
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1949
            'functionCall' => [MathTrig::class, 'SUMX2MY2'],
1950
            'argumentCount' => '2',
1951
        ],
1952
        'SUMX2PY2' => [
1953
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1954
            'functionCall' => [MathTrig::class, 'SUMX2PY2'],
1955
            'argumentCount' => '2',
1956
        ],
1957
        'SUMXMY2' => [
1958
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1959
            'functionCall' => [MathTrig::class, 'SUMXMY2'],
1960
            'argumentCount' => '2',
1961
        ],
1962
        'SWITCH' => [
1963
            'category' => Category::CATEGORY_LOGICAL,
1964
            'functionCall' => [Logical::class, 'statementSwitch'],
1965
            'argumentCount' => '3+',
1966
        ],
1967
        'SYD' => [
1968
            'category' => Category::CATEGORY_FINANCIAL,
1969
            'functionCall' => [Financial::class, 'SYD'],
1970
            'argumentCount' => '4',
1971
        ],
1972
        'T' => [
1973
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1974
            'functionCall' => [TextData::class, 'RETURNSTRING'],
1975
            'argumentCount' => '1',
1976
        ],
1977
        'TAN' => [
1978
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1979
            'functionCall' => 'tan',
1980
            'argumentCount' => '1',
1981
        ],
1982
        'TANH' => [
1983
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1984
            'functionCall' => 'tanh',
1985
            'argumentCount' => '1',
1986
        ],
1987
        'TBILLEQ' => [
1988
            'category' => Category::CATEGORY_FINANCIAL,
1989
            'functionCall' => [Financial::class, 'TBILLEQ'],
1990
            'argumentCount' => '3',
1991
        ],
1992
        'TBILLPRICE' => [
1993
            'category' => Category::CATEGORY_FINANCIAL,
1994
            'functionCall' => [Financial::class, 'TBILLPRICE'],
1995
            'argumentCount' => '3',
1996
        ],
1997
        'TBILLYIELD' => [
1998
            'category' => Category::CATEGORY_FINANCIAL,
1999
            'functionCall' => [Financial::class, 'TBILLYIELD'],
2000
            'argumentCount' => '3',
2001
        ],
2002
        'TDIST' => [
2003
            'category' => Category::CATEGORY_STATISTICAL,
2004
            'functionCall' => [Statistical::class, 'TDIST'],
2005
            'argumentCount' => '3',
2006
        ],
2007
        'TEXT' => [
2008
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2009
            'functionCall' => [TextData::class, 'TEXTFORMAT'],
2010
            'argumentCount' => '2',
2011
        ],
2012
        'TEXTJOIN' => [
2013
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2014
            'functionCall' => [TextData::class, 'TEXTJOIN'],
2015
            'argumentCount' => '3+',
2016
        ],
2017
        'TIME' => [
2018
            'category' => Category::CATEGORY_DATE_AND_TIME,
2019
            'functionCall' => [DateTime::class, 'TIME'],
2020
            'argumentCount' => '3',
2021
        ],
2022
        'TIMEVALUE' => [
2023
            'category' => Category::CATEGORY_DATE_AND_TIME,
2024
            'functionCall' => [DateTime::class, 'TIMEVALUE'],
2025
            'argumentCount' => '1',
2026
        ],
2027
        'TINV' => [
2028
            'category' => Category::CATEGORY_STATISTICAL,
2029
            'functionCall' => [Statistical::class, 'TINV'],
2030
            'argumentCount' => '2',
2031
        ],
2032
        'TODAY' => [
2033
            'category' => Category::CATEGORY_DATE_AND_TIME,
2034
            'functionCall' => [DateTime::class, 'DATENOW'],
2035
            'argumentCount' => '0',
2036
        ],
2037
        'TRANSPOSE' => [
2038
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2039
            'functionCall' => [LookupRef::class, 'TRANSPOSE'],
2040
            'argumentCount' => '1',
2041
        ],
2042
        'TREND' => [
2043
            'category' => Category::CATEGORY_STATISTICAL,
2044
            'functionCall' => [Statistical::class, 'TREND'],
2045
            'argumentCount' => '1-4',
2046
        ],
2047
        'TRIM' => [
2048
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2049
            'functionCall' => [TextData::class, 'TRIMSPACES'],
2050
            'argumentCount' => '1',
2051
        ],
2052
        'TRIMMEAN' => [
2053
            'category' => Category::CATEGORY_STATISTICAL,
2054
            'functionCall' => [Statistical::class, 'TRIMMEAN'],
2055
            'argumentCount' => '2',
2056
        ],
2057
        'TRUE' => [
2058
            'category' => Category::CATEGORY_LOGICAL,
2059
            'functionCall' => [Logical::class, 'TRUE'],
2060
            'argumentCount' => '0',
2061
        ],
2062
        'TRUNC' => [
2063
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2064
            'functionCall' => [MathTrig::class, 'TRUNC'],
2065
            'argumentCount' => '1,2',
2066
        ],
2067
        'TTEST' => [
2068
            'category' => Category::CATEGORY_STATISTICAL,
2069
            'functionCall' => [Functions::class, 'DUMMY'],
2070
            'argumentCount' => '4',
2071
        ],
2072
        'TYPE' => [
2073
            'category' => Category::CATEGORY_INFORMATION,
2074
            'functionCall' => [Functions::class, 'TYPE'],
2075
            'argumentCount' => '1',
2076
        ],
2077
        'UNICHAR' => [
2078
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2079
            'functionCall' => [TextData::class, 'CHARACTER'],
2080
            'argumentCount' => '1',
2081
        ],
2082
        'UNICODE' => [
2083
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2084
            'functionCall' => [TextData::class, 'ASCIICODE'],
2085
            'argumentCount' => '1',
2086
        ],
2087
        'UPPER' => [
2088
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2089
            'functionCall' => [TextData::class, 'UPPERCASE'],
2090
            'argumentCount' => '1',
2091
        ],
2092
        'USDOLLAR' => [
2093
            'category' => Category::CATEGORY_FINANCIAL,
2094
            'functionCall' => [Functions::class, 'DUMMY'],
2095
            'argumentCount' => '2',
2096
        ],
2097
        'VALUE' => [
2098
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2099
            'functionCall' => [TextData::class, 'VALUE'],
2100
            'argumentCount' => '1',
2101
        ],
2102
        'VAR' => [
2103
            'category' => Category::CATEGORY_STATISTICAL,
2104
            'functionCall' => [Statistical::class, 'VARFunc'],
2105
            'argumentCount' => '1+',
2106
        ],
2107
        'VAR.P' => [
2108
            'category' => Category::CATEGORY_STATISTICAL,
2109
            'functionCall' => [Statistical::class, 'VARP'],
2110
            'argumentCount' => '1+',
2111
        ],
2112
        'VAR.S' => [
2113
            'category' => Category::CATEGORY_STATISTICAL,
2114
            'functionCall' => [Statistical::class, 'VARFunc'],
2115
            'argumentCount' => '1+',
2116
        ],
2117
        'VARA' => [
2118
            'category' => Category::CATEGORY_STATISTICAL,
2119
            'functionCall' => [Statistical::class, 'VARA'],
2120
            'argumentCount' => '1+',
2121
        ],
2122
        'VARP' => [
2123
            'category' => Category::CATEGORY_STATISTICAL,
2124
            'functionCall' => [Statistical::class, 'VARP'],
2125
            'argumentCount' => '1+',
2126
        ],
2127
        'VARPA' => [
2128
            'category' => Category::CATEGORY_STATISTICAL,
2129
            'functionCall' => [Statistical::class, 'VARPA'],
2130
            'argumentCount' => '1+',
2131
        ],
2132
        'VDB' => [
2133
            'category' => Category::CATEGORY_FINANCIAL,
2134
            'functionCall' => [Functions::class, 'DUMMY'],
2135
            'argumentCount' => '5-7',
2136
        ],
2137
        'VLOOKUP' => [
2138
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2139
            'functionCall' => [LookupRef::class, 'VLOOKUP'],
2140
            'argumentCount' => '3,4',
2141
        ],
2142
        'WEEKDAY' => [
2143
            'category' => Category::CATEGORY_DATE_AND_TIME,
2144
            'functionCall' => [DateTime::class, 'WEEKDAY'],
2145
            'argumentCount' => '1,2',
2146
        ],
2147
        'WEEKNUM' => [
2148
            'category' => Category::CATEGORY_DATE_AND_TIME,
2149
            'functionCall' => [DateTime::class, 'WEEKNUM'],
2150
            'argumentCount' => '1,2',
2151
        ],
2152
        'WEIBULL' => [
2153
            'category' => Category::CATEGORY_STATISTICAL,
2154
            'functionCall' => [Statistical::class, 'WEIBULL'],
2155
            'argumentCount' => '4',
2156
        ],
2157
        'WORKDAY' => [
2158
            'category' => Category::CATEGORY_DATE_AND_TIME,
2159
            'functionCall' => [DateTime::class, 'WORKDAY'],
2160
            'argumentCount' => '2+',
2161
        ],
2162
        'XIRR' => [
2163
            'category' => Category::CATEGORY_FINANCIAL,
2164
            'functionCall' => [Financial::class, 'XIRR'],
2165
            'argumentCount' => '2,3',
2166
        ],
2167
        'XNPV' => [
2168
            'category' => Category::CATEGORY_FINANCIAL,
2169
            'functionCall' => [Financial::class, 'XNPV'],
2170
            'argumentCount' => '3',
2171
        ],
2172
        'XOR' => [
2173
            'category' => Category::CATEGORY_LOGICAL,
2174
            'functionCall' => [Logical::class, 'logicalXor'],
2175
            'argumentCount' => '1+',
2176
        ],
2177
        'YEAR' => [
2178
            'category' => Category::CATEGORY_DATE_AND_TIME,
2179
            'functionCall' => [DateTime::class, 'YEAR'],
2180
            'argumentCount' => '1',
2181
        ],
2182
        'YEARFRAC' => [
2183
            'category' => Category::CATEGORY_DATE_AND_TIME,
2184
            'functionCall' => [DateTime::class, 'YEARFRAC'],
2185
            'argumentCount' => '2,3',
2186
        ],
2187
        'YIELD' => [
2188
            'category' => Category::CATEGORY_FINANCIAL,
2189
            'functionCall' => [Functions::class, 'DUMMY'],
2190
            'argumentCount' => '6,7',
2191
        ],
2192
        'YIELDDISC' => [
2193
            'category' => Category::CATEGORY_FINANCIAL,
2194
            'functionCall' => [Financial::class, 'YIELDDISC'],
2195
            'argumentCount' => '4,5',
2196
        ],
2197
        'YIELDMAT' => [
2198
            'category' => Category::CATEGORY_FINANCIAL,
2199
            'functionCall' => [Financial::class, 'YIELDMAT'],
2200
            'argumentCount' => '5,6',
2201
        ],
2202
        'ZTEST' => [
2203
            'category' => Category::CATEGORY_STATISTICAL,
2204
            'functionCall' => [Statistical::class, 'ZTEST'],
2205
            'argumentCount' => '2-3',
2206
        ],
2207
    ];
2208
2209
    //    Internal functions used for special control purposes
2210
    private static $controlFunctions = [
2211
        'MKMATRIX' => [
2212
            'argumentCount' => '*',
2213
            'functionCall' => 'self::mkMatrix',
2214
        ],
2215
    ];
2216
2217 239
    public function __construct(Spreadsheet $spreadsheet = null)
2218
    {
2219 239
        $this->delta = 1 * pow(10, 0 - ini_get('precision'));
2220
2221 239
        $this->spreadsheet = $spreadsheet;
2222 239
        $this->cyclicReferenceStack = new CyclicReferenceStack();
2223 239
        $this->debugLog = new Logger($this->cyclicReferenceStack);
2224 239
    }
2225
2226 1
    private static function loadLocales()
2227
    {
2228 1
        $localeFileDirectory = __DIR__ . '/locale/';
2229 1
        foreach (glob($localeFileDirectory . '*', GLOB_ONLYDIR) as $filename) {
2230 1
            $filename = substr($filename, strlen($localeFileDirectory));
2231 1
            if ($filename != 'en') {
2232 1
                self::$validLocaleLanguages[] = $filename;
2233
            }
2234
        }
2235 1
    }
2236
2237
    /**
2238
     * Get an instance of this class.
2239
     *
2240
     * @param Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object,
2241
     *                                    or NULL to create a standalone claculation engine
2242
     *
2243
     * @return Calculation
2244
     */
2245 675
    public static function getInstance(Spreadsheet $spreadsheet = null)
2246
    {
2247 675
        if ($spreadsheet !== null) {
2248 144
            $instance = $spreadsheet->getCalculationEngine();
2249 144
            if (isset($instance)) {
2250 144
                return $instance;
2251
            }
2252
        }
2253
2254 558
        if (!isset(self::$instance) || (self::$instance === null)) {
2255 14
            self::$instance = new self();
2256
        }
2257
2258 558
        return self::$instance;
2259
    }
2260
2261
    /**
2262
     * Flush the calculation cache for any existing instance of this class
2263
     *        but only if a Calculation instance exists.
2264
     */
2265 4
    public function flushInstance()
2266
    {
2267 4
        $this->clearCalculationCache();
2268 4
        $this->clearBranchStore();
2269 4
    }
2270
2271
    /**
2272
     * Get the Logger for this calculation engine instance.
2273
     *
2274
     * @return Logger
2275
     */
2276 102
    public function getDebugLog()
2277
    {
2278 102
        return $this->debugLog;
2279
    }
2280
2281
    /**
2282
     * __clone implementation. Cloning should not be allowed in a Singleton!
2283
     *
2284
     * @throws Exception
2285
     */
2286
    final public function __clone()
2287
    {
2288
        throw new Exception('Cloning the calculation engine is not allowed!');
2289
    }
2290
2291
    /**
2292
     * Return the locale-specific translation of TRUE.
2293
     *
2294
     * @return string locale-specific translation of TRUE
2295
     */
2296 42
    public static function getTRUE()
2297
    {
2298 42
        return self::$localeBoolean['TRUE'];
2299
    }
2300
2301
    /**
2302
     * Return the locale-specific translation of FALSE.
2303
     *
2304
     * @return string locale-specific translation of FALSE
2305
     */
2306 31
    public static function getFALSE()
2307
    {
2308 31
        return self::$localeBoolean['FALSE'];
2309
    }
2310
2311
    /**
2312
     * Set the Array Return Type (Array or Value of first element in the array).
2313
     *
2314
     * @param string $returnType Array return type
2315
     *
2316
     * @return bool Success or failure
2317
     */
2318 15
    public static function setArrayReturnType($returnType)
2319
    {
2320 15
        if (($returnType == self::RETURN_ARRAY_AS_VALUE) ||
2321
            ($returnType == self::RETURN_ARRAY_AS_ERROR) ||
2322 15
            ($returnType == self::RETURN_ARRAY_AS_ARRAY)) {
2323 15
            self::$returnArrayAsType = $returnType;
2324
2325 15
            return true;
2326
        }
2327
2328
        return false;
2329
    }
2330
2331
    /**
2332
     * Return the Array Return Type (Array or Value of first element in the array).
2333
     *
2334
     * @return string $returnType Array return type
2335
     */
2336 15
    public static function getArrayReturnType()
2337
    {
2338 15
        return self::$returnArrayAsType;
2339
    }
2340
2341
    /**
2342
     * Is calculation caching enabled?
2343
     *
2344
     * @return bool
2345
     */
2346 1
    public function getCalculationCacheEnabled()
2347
    {
2348 1
        return $this->calculationCacheEnabled;
2349
    }
2350
2351
    /**
2352
     * Enable/disable calculation cache.
2353
     *
2354
     * @param bool $pValue
2355
     */
2356
    public function setCalculationCacheEnabled($pValue)
2357
    {
2358
        $this->calculationCacheEnabled = $pValue;
2359
        $this->clearCalculationCache();
2360
    }
2361
2362
    /**
2363
     * Enable calculation cache.
2364
     */
2365
    public function enableCalculationCache()
2366
    {
2367
        $this->setCalculationCacheEnabled(true);
2368
    }
2369
2370
    /**
2371
     * Disable calculation cache.
2372
     */
2373
    public function disableCalculationCache()
2374
    {
2375
        $this->setCalculationCacheEnabled(false);
2376
    }
2377
2378
    /**
2379
     * Clear calculation cache.
2380
     */
2381 4
    public function clearCalculationCache()
2382
    {
2383 4
        $this->calculationCache = [];
2384 4
    }
2385
2386
    /**
2387
     * Clear calculation cache for a specified worksheet.
2388
     *
2389
     * @param string $worksheetName
2390
     */
2391 27
    public function clearCalculationCacheForWorksheet($worksheetName)
2392
    {
2393 27
        if (isset($this->calculationCache[$worksheetName])) {
2394
            unset($this->calculationCache[$worksheetName]);
2395
        }
2396 27
    }
2397
2398
    /**
2399
     * Rename calculation cache for a specified worksheet.
2400
     *
2401
     * @param string $fromWorksheetName
2402
     * @param string $toWorksheetName
2403
     */
2404 239
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName)
2405
    {
2406 239
        if (isset($this->calculationCache[$fromWorksheetName])) {
2407
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2408
            unset($this->calculationCache[$fromWorksheetName]);
2409
        }
2410 239
    }
2411
2412
    /**
2413
     * Enable/disable calculation cache.
2414
     *
2415
     * @param bool $pValue
2416
     * @param mixed $enabled
2417
     */
2418 9
    public function setBranchPruningEnabled($enabled)
2419
    {
2420 9
        $this->branchPruningEnabled = $enabled;
2421 9
    }
2422
2423
    public function enableBranchPruning()
2424
    {
2425
        $this->setBranchPruningEnabled(true);
2426
    }
2427
2428 9
    public function disableBranchPruning()
2429
    {
2430 9
        $this->setBranchPruningEnabled(false);
2431 9
    }
2432
2433 4
    public function clearBranchStore()
2434
    {
2435 4
        $this->branchStoreKeyCounter = 0;
2436 4
    }
2437
2438
    /**
2439
     * Get the currently defined locale code.
2440
     *
2441
     * @return string
2442
     */
2443
    public function getLocale()
2444
    {
2445
        return self::$localeLanguage;
2446
    }
2447
2448
    /**
2449
     * Set the locale code.
2450
     *
2451
     * @param string $locale The locale to use for formula translation, eg: 'en_us'
2452
     *
2453
     * @return bool
2454
     */
2455 486
    public function setLocale($locale)
2456
    {
2457
        //    Identify our locale and language
2458 486
        $language = $locale = strtolower($locale);
2459 486
        if (strpos($locale, '_') !== false) {
2460 486
            [$language] = explode('_', $locale);
2461
        }
2462 486
        if (count(self::$validLocaleLanguages) == 1) {
2463 1
            self::loadLocales();
2464
        }
2465
        //    Test whether we have any language data for this language (any locale)
2466 486
        if (in_array($language, self::$validLocaleLanguages)) {
2467
            //    initialise language/locale settings
2468 486
            self::$localeFunctions = [];
2469 486
            self::$localeArgumentSeparator = ',';
2470 486
            self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL'];
2471
            //    Default is English, if user isn't requesting english, then read the necessary data from the locale files
2472 486
            if ($locale != 'en_us') {
2473
                //    Search for a file with a list of function names for locale
2474 17
                $functionNamesFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'functions';
2475 17
                if (!file_exists($functionNamesFile)) {
2476
                    //    If there isn't a locale specific function file, look for a language specific function file
2477
                    $functionNamesFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'functions';
2478
                    if (!file_exists($functionNamesFile)) {
2479
                        return false;
2480
                    }
2481
                }
2482
                //    Retrieve the list of locale or language specific function names
2483 17
                $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2484 17
                foreach ($localeFunctions as $localeFunction) {
2485 17
                    [$localeFunction] = explode('##', $localeFunction); //    Strip out comments
2486 17
                    if (strpos($localeFunction, '=') !== false) {
2487 17
                        [$fName, $lfName] = explode('=', $localeFunction);
2488 17
                        $fName = trim($fName);
2489 17
                        $lfName = trim($lfName);
2490 17
                        if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) {
2491 17
                            self::$localeFunctions[$fName] = $lfName;
2492
                        }
2493
                    }
2494
                }
2495
                //    Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions
2496 17
                if (isset(self::$localeFunctions['TRUE'])) {
2497 17
                    self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE'];
2498
                }
2499 17
                if (isset(self::$localeFunctions['FALSE'])) {
2500 17
                    self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE'];
2501
                }
2502
2503 17
                $configFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'config';
2504 17
                if (!file_exists($configFile)) {
2505
                    $configFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'config';
2506
                }
2507 17
                if (file_exists($configFile)) {
2508 17
                    $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2509 17
                    foreach ($localeSettings as $localeSetting) {
2510 17
                        [$localeSetting] = explode('##', $localeSetting); //    Strip out comments
2511 17
                        if (strpos($localeSetting, '=') !== false) {
2512 17
                            [$settingName, $settingValue] = explode('=', $localeSetting);
2513 17
                            $settingName = strtoupper(trim($settingName));
2514
                            switch ($settingName) {
2515 17
                                case 'ARGUMENTSEPARATOR':
2516 17
                                    self::$localeArgumentSeparator = trim($settingValue);
2517
2518 17
                                    break;
2519
                            }
2520
                        }
2521
                    }
2522
                }
2523
            }
2524
2525
            self::$functionReplaceFromExcel = self::$functionReplaceToExcel =
2526 486
            self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
2527 486
            self::$localeLanguage = $locale;
2528
2529 486
            return true;
2530
        }
2531
2532
        return false;
2533
    }
2534
2535
    /**
2536
     * @param string $fromSeparator
2537
     * @param string $toSeparator
2538
     * @param string $formula
2539
     * @param bool $inBraces
2540
     *
2541
     * @return string
2542
     */
2543 5
    public static function translateSeparator($fromSeparator, $toSeparator, $formula, &$inBraces)
2544
    {
2545 5
        $strlen = mb_strlen($formula);
2546 5
        for ($i = 0; $i < $strlen; ++$i) {
2547 5
            $chr = mb_substr($formula, $i, 1);
2548
            switch ($chr) {
2549 5
                case '{':
2550
                    $inBraces = true;
2551
2552
                    break;
2553 5
                case '}':
2554
                    $inBraces = false;
2555
2556
                    break;
2557 5
                case $fromSeparator:
2558
                    if (!$inBraces) {
2559
                        $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
2560
                    }
2561
            }
2562
        }
2563
2564 5
        return $formula;
2565
    }
2566
2567
    /**
2568
     * @param string[] $from
2569
     * @param string[] $to
2570
     * @param string $formula
2571
     * @param string $fromSeparator
2572
     * @param string $toSeparator
2573
     *
2574
     * @return string
2575
     */
2576
    private static function translateFormula(array $from, array $to, $formula, $fromSeparator, $toSeparator)
2577
    {
2578
        //    Convert any Excel function names to the required language
2579
        if (self::$localeLanguage !== 'en_us') {
2580
            $inBraces = false;
2581
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
2582
            if (strpos($formula, '"') !== false) {
2583
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
2584
                //        the formula
2585
                $temp = explode('"', $formula);
2586
                $i = false;
2587
                foreach ($temp as &$value) {
2588
                    //    Only count/replace in alternating array entries
2589
                    if ($i = !$i) {
0 ignored issues
show
introduced by
The condition $i is always false.
Loading history...
2590
                        $value = preg_replace($from, $to, $value);
2591
                        $value = self::translateSeparator($fromSeparator, $toSeparator, $value, $inBraces);
2592
                    }
2593
                }
2594
                unset($value);
2595
                //    Then rebuild the formula string
2596
                $formula = implode('"', $temp);
2597
            } else {
2598
                //    If there's no quoted strings, then we do a simple count/replace
2599
                $formula = preg_replace($from, $to, $formula);
2600
                $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inBraces);
2601
            }
2602
        }
2603
2604
        return $formula;
2605
    }
2606
2607
    private static $functionReplaceFromExcel = null;
2608
2609
    private static $functionReplaceToLocale = null;
2610
2611
    public function _translateFormulaToLocale($formula)
2612
    {
2613
        if (self::$functionReplaceFromExcel === null) {
2614
            self::$functionReplaceFromExcel = [];
2615
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2616
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/Ui';
2617
            }
2618
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2619
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui';
2620
            }
2621
        }
2622
2623
        if (self::$functionReplaceToLocale === null) {
2624
            self::$functionReplaceToLocale = [];
2625
            foreach (self::$localeFunctions as $localeFunctionName) {
2626
                self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2';
2627
            }
2628
            foreach (self::$localeBoolean as $localeBoolean) {
2629
                self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2';
2630
            }
2631
        }
2632
2633
        return self::translateFormula(self::$functionReplaceFromExcel, self::$functionReplaceToLocale, $formula, ',', self::$localeArgumentSeparator);
2634
    }
2635
2636
    private static $functionReplaceFromLocale = null;
2637
2638
    private static $functionReplaceToExcel = null;
2639
2640
    public function _translateFormulaToEnglish($formula)
2641
    {
2642
        if (self::$functionReplaceFromLocale === null) {
2643
            self::$functionReplaceFromLocale = [];
2644
            foreach (self::$localeFunctions as $localeFunctionName) {
2645
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/Ui';
2646
            }
2647
            foreach (self::$localeBoolean as $excelBoolean) {
2648
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui';
2649
            }
2650
        }
2651
2652
        if (self::$functionReplaceToExcel === null) {
2653
            self::$functionReplaceToExcel = [];
2654
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2655
                self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2';
2656
            }
2657
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2658
                self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2';
2659
            }
2660
        }
2661
2662
        return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ',');
2663
    }
2664
2665 63
    public static function localeFunc($function)
2666
    {
2667 63
        if (self::$localeLanguage !== 'en_us') {
2668
            $functionName = trim($function, '(');
2669
            if (isset(self::$localeFunctions[$functionName])) {
2670
                $brace = ($functionName != $function);
2671
                $function = self::$localeFunctions[$functionName];
2672
                if ($brace) {
2673
                    $function .= '(';
2674
                }
2675
            }
2676
        }
2677
2678 63
        return $function;
2679
    }
2680
2681
    /**
2682
     * Wrap string values in quotes.
2683
     *
2684
     * @param mixed $value
2685
     *
2686
     * @return mixed
2687
     */
2688 121
    public static function wrapResult($value)
2689
    {
2690 121
        if (is_string($value)) {
2691
            //    Error values cannot be "wrapped"
2692 102
            if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
2693
                //    Return Excel errors "as is"
2694 10
                return $value;
2695
            }
2696
            //    Return strings wrapped in quotes
2697 93
            return '"' . $value . '"';
2698
        //    Convert numeric errors to NaN error
2699 56
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2700
            return Functions::NAN();
2701
        }
2702
2703 56
        return $value;
2704
    }
2705
2706
    /**
2707
     * Remove quotes used as a wrapper to identify string values.
2708
     *
2709
     * @param mixed $value
2710
     *
2711
     * @return mixed
2712
     */
2713 128
    public static function unwrapResult($value)
2714
    {
2715 128
        if (is_string($value)) {
2716 103
            if ((isset($value[0])) && ($value[0] == '"') && (substr($value, -1) == '"')) {
2717 103
                return substr($value, 1, -1);
2718
            }
2719
            //    Convert numeric errors to NAN error
2720 72
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2721
            return Functions::NAN();
2722
        }
2723
2724 73
        return $value;
2725
    }
2726
2727
    /**
2728
     * Calculate cell value (using formula from a cell ID)
2729
     * Retained for backward compatibility.
2730
     *
2731
     * @param Cell $pCell Cell to calculate
2732
     *
2733
     * @throws Exception
2734
     *
2735
     * @return mixed
2736
     */
2737
    public function calculate(Cell $pCell = null)
2738
    {
2739
        try {
2740
            return $this->calculateCellValue($pCell);
2741
        } catch (\Exception $e) {
2742
            throw new Exception($e->getMessage());
2743
        }
2744
    }
2745
2746
    /**
2747
     * Calculate the value of a cell formula.
2748
     *
2749
     * @param Cell $pCell Cell to calculate
2750
     * @param bool $resetLog Flag indicating whether the debug log should be reset or not
2751
     *
2752
     * @throws \PhpOffice\PhpSpreadsheet\Exception
2753
     *
2754
     * @return mixed
2755
     */
2756 63
    public function calculateCellValue(Cell $pCell = null, $resetLog = true)
2757
    {
2758 63
        if ($pCell === null) {
2759
            return null;
2760
        }
2761
2762 63
        $returnArrayAsType = self::$returnArrayAsType;
2763 63
        if ($resetLog) {
2764
            //    Initialise the logging settings if requested
2765 62
            $this->formulaError = null;
2766 62
            $this->debugLog->clearLog();
2767 62
            $this->cyclicReferenceStack->clear();
2768 62
            $this->cyclicFormulaCounter = 1;
2769
2770 62
            self::$returnArrayAsType = self::RETURN_ARRAY_AS_ARRAY;
2771
        }
2772
2773
        //    Execute the calculation for the cell formula
2774 63
        $this->cellStack[] = [
2775 63
            'sheet' => $pCell->getWorksheet()->getTitle(),
2776 63
            'cell' => $pCell->getCoordinate(),
2777
        ];
2778
2779
        try {
2780 63
            $result = self::unwrapResult($this->_calculateFormulaValue($pCell->getValue(), $pCell->getCoordinate(), $pCell));
2781 63
            $cellAddress = array_pop($this->cellStack);
2782 63
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2783
        } catch (\Exception $e) {
2784
            $cellAddress = array_pop($this->cellStack);
2785
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2786
2787
            throw new Exception($e->getMessage());
2788
        }
2789
2790 63
        if ((is_array($result)) && (self::$returnArrayAsType != self::RETURN_ARRAY_AS_ARRAY)) {
2791 2
            self::$returnArrayAsType = $returnArrayAsType;
2792 2
            $testResult = Functions::flattenArray($result);
2793 2
            if (self::$returnArrayAsType == self::RETURN_ARRAY_AS_ERROR) {
2794
                return Functions::VALUE();
2795
            }
2796
            //    If there's only a single cell in the array, then we allow it
2797 2
            if (count($testResult) != 1) {
2798
                //    If keys are numeric, then it's a matrix result rather than a cell range result, so we permit it
2799
                $r = array_keys($result);
2800
                $r = array_shift($r);
2801
                if (!is_numeric($r)) {
2802
                    return Functions::VALUE();
2803
                }
2804
                if (is_array($result[$r])) {
2805
                    $c = array_keys($result[$r]);
2806
                    $c = array_shift($c);
2807
                    if (!is_numeric($c)) {
2808
                        return Functions::VALUE();
2809
                    }
2810
                }
2811
            }
2812 2
            $result = array_shift($testResult);
2813
        }
2814 63
        self::$returnArrayAsType = $returnArrayAsType;
2815
2816 63
        if ($result === null) {
2817
            return 0;
2818 63
        } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
2819
            return Functions::NAN();
2820
        }
2821
2822 63
        return $result;
2823
    }
2824
2825
    /**
2826
     * Validate and parse a formula string.
2827
     *
2828
     * @param string $formula Formula to parse
2829
     *
2830
     * @return array|bool
2831
     */
2832 5
    public function parseFormula($formula)
2833
    {
2834
        //    Basic validation that this is indeed a formula
2835
        //    We return an empty array if not
2836 5
        $formula = trim($formula);
2837 5
        if ((!isset($formula[0])) || ($formula[0] != '=')) {
2838
            return [];
2839
        }
2840 5
        $formula = ltrim(substr($formula, 1));
2841 5
        if (!isset($formula[0])) {
2842
            return [];
2843
        }
2844
2845
        //    Parse the formula and return the token stack
2846 5
        return $this->_parseFormula($formula);
2847
    }
2848
2849
    /**
2850
     * Calculate the value of a formula.
2851
     *
2852
     * @param string $formula Formula to parse
2853
     * @param string $cellID Address of the cell to calculate
2854
     * @param Cell $pCell Cell to calculate
2855
     *
2856
     * @throws \PhpOffice\PhpSpreadsheet\Exception
2857
     *
2858
     * @return mixed
2859
     */
2860 1
    public function calculateFormula($formula, $cellID = null, Cell $pCell = null)
2861
    {
2862
        //    Initialise the logging settings
2863 1
        $this->formulaError = null;
2864 1
        $this->debugLog->clearLog();
2865 1
        $this->cyclicReferenceStack->clear();
2866
2867 1
        if ($this->spreadsheet !== null && $cellID === null && $pCell === null) {
2868
            $cellID = 'A1';
2869
            $pCell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
2870
        } else {
2871
            //    Disable calculation cacheing because it only applies to cell calculations, not straight formulae
2872
            //    But don't actually flush any cache
2873 1
            $resetCache = $this->getCalculationCacheEnabled();
2874 1
            $this->calculationCacheEnabled = false;
2875
        }
2876
2877
        //    Execute the calculation
2878
        try {
2879 1
            $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $pCell));
2880 1
        } catch (\Exception $e) {
2881 1
            throw new Exception($e->getMessage());
2882
        }
2883
2884 1
        if ($this->spreadsheet === null) {
2885
            //    Reset calculation cacheing to its previous state
2886
            $this->calculationCacheEnabled = $resetCache;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $resetCache does not seem to be defined for all execution paths leading up to this point.
Loading history...
2887
        }
2888
2889 1
        return $result;
2890
    }
2891
2892
    /**
2893
     * @param string $cellReference
2894
     * @param mixed $cellValue
2895
     *
2896
     * @return bool
2897
     */
2898 62
    public function getValueFromCache($cellReference, &$cellValue)
2899
    {
2900
        // Is calculation cacheing enabled?
2901
        // Is the value present in calculation cache?
2902 62
        $this->debugLog->writeDebugLog('Testing cache value for cell ', $cellReference);
2903 62
        if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
2904 51
            $this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache');
2905
            // Return the cached result
2906
2907 51
            $cellValue = $this->calculationCache[$cellReference];
2908
2909 51
            return true;
2910
        }
2911
2912 62
        return false;
2913
    }
2914
2915
    /**
2916
     * @param string $cellReference
2917
     * @param mixed $cellValue
2918
     */
2919 62
    public function saveValueToCache($cellReference, $cellValue)
2920
    {
2921 62
        if ($this->calculationCacheEnabled) {
2922 61
            $this->calculationCache[$cellReference] = $cellValue;
2923
        }
2924 62
    }
2925
2926
    /**
2927
     * Parse a cell formula and calculate its value.
2928
     *
2929
     * @param string $formula The formula to parse and calculate
2930
     * @param string $cellID The ID (e.g. A3) of the cell that we are calculating
2931
     * @param Cell $pCell Cell to calculate
2932
     *
2933
     * @throws Exception
2934
     *
2935
     * @return mixed
2936
     */
2937 164
    public function _calculateFormulaValue($formula, $cellID = null, Cell $pCell = null)
2938
    {
2939 164
        $cellValue = null;
2940
2941
        //  Quote-Prefixed cell values cannot be formulae, but are treated as strings
2942 164
        if ($pCell !== null && $pCell->getStyle()->getQuotePrefix() === true) {
2943 1
            return self::wrapResult((string) $formula);
2944
        }
2945
2946 163
        if (preg_match('/^=\s*cmd\s*\|/miu', $formula) !== 0) {
2947 1
            return self::wrapResult($formula);
2948
        }
2949
2950
        //    Basic validation that this is indeed a formula
2951
        //    We simply return the cell value if not
2952 162
        $formula = trim($formula);
2953 162
        if ($formula[0] != '=') {
2954
            return self::wrapResult($formula);
2955
        }
2956 162
        $formula = ltrim(substr($formula, 1));
2957 162
        if (!isset($formula[0])) {
2958
            return self::wrapResult($formula);
2959
        }
2960
2961 162
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
2962 162
        $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
2963 162
        $wsCellReference = $wsTitle . '!' . $cellID;
2964
2965 162
        if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
2966 51
            return $cellValue;
2967
        }
2968
2969 162
        if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) {
2970
            if ($this->cyclicFormulaCount <= 0) {
2971
                $this->cyclicFormulaCell = '';
2972
2973
                return $this->raiseFormulaError('Cyclic Reference in Formula');
2974
            } elseif ($this->cyclicFormulaCell === $wsCellReference) {
2975
                ++$this->cyclicFormulaCounter;
2976
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2977
                    $this->cyclicFormulaCell = '';
2978
2979
                    return $cellValue;
2980
                }
2981
            } elseif ($this->cyclicFormulaCell == '') {
2982
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2983
                    return $cellValue;
2984
                }
2985
                $this->cyclicFormulaCell = $wsCellReference;
2986
            }
2987
        }
2988
2989
        //    Parse the formula onto the token stack and calculate the value
2990 162
        $this->cyclicReferenceStack->push($wsCellReference);
2991 162
        $cellValue = $this->processTokenStack($this->_parseFormula($formula, $pCell), $cellID, $pCell);
2992 162
        $this->cyclicReferenceStack->pop();
2993
2994
        // Save to calculation cache
2995 162
        if ($cellID !== null) {
2996 62
            $this->saveValueToCache($wsCellReference, $cellValue);
2997
        }
2998
2999
        //    Return the calculated value
3000 162
        return $cellValue;
3001
    }
3002
3003
    /**
3004
     * Ensure that paired matrix operands are both matrices and of the same size.
3005
     *
3006
     * @param mixed &$operand1 First matrix operand
3007
     * @param mixed &$operand2 Second matrix operand
3008
     * @param int $resize Flag indicating whether the matrices should be resized to match
3009
     *                                        and (if so), whether the smaller dimension should grow or the
3010
     *                                        larger should shrink.
3011
     *                                            0 = no resize
3012
     *                                            1 = shrink to fit
3013
     *                                            2 = extend to fit
3014
     *
3015
     * @return array
3016
     */
3017 7
    private static function checkMatrixOperands(&$operand1, &$operand2, $resize = 1)
3018
    {
3019
        //    Examine each of the two operands, and turn them into an array if they aren't one already
3020
        //    Note that this function should only be called if one or both of the operand is already an array
3021 7
        if (!is_array($operand1)) {
3022
            [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand2);
3023
            $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1));
3024
            $resize = 0;
3025 7
        } elseif (!is_array($operand2)) {
3026 2
            [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand1);
3027 2
            $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2));
3028 2
            $resize = 0;
3029
        }
3030
3031 7
        [$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1);
3032 7
        [$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2);
3033 7
        if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
3034 7
            $resize = 1;
3035
        }
3036
3037 7
        if ($resize == 2) {
3038
            //    Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger
3039
            self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
3040 7
        } elseif ($resize == 1) {
3041
            //    Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller
3042 7
            self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
3043
        }
3044
3045 7
        return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns];
3046
    }
3047
3048
    /**
3049
     * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0.
3050
     *
3051
     * @param array &$matrix matrix operand
3052
     *
3053
     * @return int[] An array comprising the number of rows, and number of columns
3054
     */
3055 17
    public static function getMatrixDimensions(array &$matrix)
3056
    {
3057 17
        $matrixRows = count($matrix);
3058 17
        $matrixColumns = 0;
3059 17
        foreach ($matrix as $rowKey => $rowValue) {
3060 15
            if (!is_array($rowValue)) {
3061 4
                $matrix[$rowKey] = [$rowValue];
3062 4
                $matrixColumns = max(1, $matrixColumns);
3063
            } else {
3064 11
                $matrix[$rowKey] = array_values($rowValue);
3065 11
                $matrixColumns = max(count($rowValue), $matrixColumns);
3066
            }
3067
        }
3068 17
        $matrix = array_values($matrix);
3069
3070 17
        return [$matrixRows, $matrixColumns];
3071
    }
3072
3073
    /**
3074
     * Ensure that paired matrix operands are both matrices of the same size.
3075
     *
3076
     * @param mixed &$matrix1 First matrix operand
3077
     * @param mixed &$matrix2 Second matrix operand
3078
     * @param int $matrix1Rows Row size of first matrix operand
3079
     * @param int $matrix1Columns Column size of first matrix operand
3080
     * @param int $matrix2Rows Row size of second matrix operand
3081
     * @param int $matrix2Columns Column size of second matrix operand
3082
     */
3083 7
    private static function resizeMatricesShrink(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
3084
    {
3085 7
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3086
            if ($matrix2Rows < $matrix1Rows) {
3087
                for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) {
3088
                    unset($matrix1[$i]);
3089
                }
3090
            }
3091
            if ($matrix2Columns < $matrix1Columns) {
3092
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3093
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3094
                        unset($matrix1[$i][$j]);
3095
                    }
3096
                }
3097
            }
3098
        }
3099
3100 7
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3101
            if ($matrix1Rows < $matrix2Rows) {
3102
                for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) {
3103
                    unset($matrix2[$i]);
3104
                }
3105
            }
3106
            if ($matrix1Columns < $matrix2Columns) {
3107
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3108
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3109
                        unset($matrix2[$i][$j]);
3110
                    }
3111
                }
3112
            }
3113
        }
3114 7
    }
3115
3116
    /**
3117
     * Ensure that paired matrix operands are both matrices of the same size.
3118
     *
3119
     * @param mixed &$matrix1 First matrix operand
3120
     * @param mixed &$matrix2 Second matrix operand
3121
     * @param int $matrix1Rows Row size of first matrix operand
3122
     * @param int $matrix1Columns Column size of first matrix operand
3123
     * @param int $matrix2Rows Row size of second matrix operand
3124
     * @param int $matrix2Columns Column size of second matrix operand
3125
     */
3126
    private static function resizeMatricesExtend(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
3127
    {
3128
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3129
            if ($matrix2Columns < $matrix1Columns) {
3130
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3131
                    $x = $matrix2[$i][$matrix2Columns - 1];
3132
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3133
                        $matrix2[$i][$j] = $x;
3134
                    }
3135
                }
3136
            }
3137
            if ($matrix2Rows < $matrix1Rows) {
3138
                $x = $matrix2[$matrix2Rows - 1];
3139
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3140
                    $matrix2[$i] = $x;
3141
                }
3142
            }
3143
        }
3144
3145
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3146
            if ($matrix1Columns < $matrix2Columns) {
3147
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3148
                    $x = $matrix1[$i][$matrix1Columns - 1];
3149
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3150
                        $matrix1[$i][$j] = $x;
3151
                    }
3152
                }
3153
            }
3154
            if ($matrix1Rows < $matrix2Rows) {
3155
                $x = $matrix1[$matrix1Rows - 1];
3156
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3157
                    $matrix1[$i] = $x;
3158
                }
3159
            }
3160
        }
3161
    }
3162
3163
    /**
3164
     * Format details of an operand for display in the log (based on operand type).
3165
     *
3166
     * @param mixed $value First matrix operand
3167
     *
3168
     * @return mixed
3169
     */
3170 159
    private function showValue($value)
3171
    {
3172 159
        if ($this->debugLog->getWriteDebugLog()) {
3173
            $testArray = Functions::flattenArray($value);
3174
            if (count($testArray) == 1) {
3175
                $value = array_pop($testArray);
3176
            }
3177
3178
            if (is_array($value)) {
3179
                $returnMatrix = [];
3180
                $pad = $rpad = ', ';
3181
                foreach ($value as $row) {
3182
                    if (is_array($row)) {
3183
                        $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
3184
                        $rpad = '; ';
3185
                    } else {
3186
                        $returnMatrix[] = $this->showValue($row);
3187
                    }
3188
                }
3189
3190
                return '{ ' . implode($rpad, $returnMatrix) . ' }';
3191
            } elseif (is_string($value) && (trim($value, '"') == $value)) {
3192
                return '"' . $value . '"';
3193
            } elseif (is_bool($value)) {
3194
                return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3195
            }
3196
        }
3197
3198 159
        return Functions::flattenSingleValue($value);
3199
    }
3200
3201
    /**
3202
     * Format type and details of an operand for display in the log (based on operand type).
3203
     *
3204
     * @param mixed $value First matrix operand
3205
     *
3206
     * @return null|string
3207
     */
3208 160
    private function showTypeDetails($value)
3209
    {
3210 160
        if ($this->debugLog->getWriteDebugLog()) {
3211
            $testArray = Functions::flattenArray($value);
3212
            if (count($testArray) == 1) {
3213
                $value = array_pop($testArray);
3214
            }
3215
3216
            if ($value === null) {
3217
                return 'a NULL value';
3218
            } elseif (is_float($value)) {
3219
                $typeString = 'a floating point number';
3220
            } elseif (is_int($value)) {
3221
                $typeString = 'an integer number';
3222
            } elseif (is_bool($value)) {
3223
                $typeString = 'a boolean';
3224
            } elseif (is_array($value)) {
3225
                $typeString = 'a matrix';
3226
            } else {
3227
                if ($value == '') {
3228
                    return 'an empty string';
3229
                } elseif ($value[0] == '#') {
3230
                    return 'a ' . $value . ' error';
3231
                }
3232
                $typeString = 'a string';
3233
            }
3234
3235
            return $typeString . ' with a value of ' . $this->showValue($value);
3236
        }
3237 160
    }
3238
3239
    /**
3240
     * @param string $formula
3241
     *
3242
     * @return string
3243
     */
3244 167
    private function convertMatrixReferences($formula)
3245
    {
3246 167
        static $matrixReplaceFrom = ['{', ';', '}'];
3247 167
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
3248
3249
        //    Convert any Excel matrix references to the MKMATRIX() function
3250 167
        if (strpos($formula, '{') !== false) {
3251
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
3252
            if (strpos($formula, '"') !== false) {
3253
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
3254
                //        the formula
3255
                $temp = explode('"', $formula);
3256
                //    Open and Closed counts used for trapping mismatched braces in the formula
3257
                $openCount = $closeCount = 0;
3258
                $i = false;
3259
                foreach ($temp as &$value) {
3260
                    //    Only count/replace in alternating array entries
3261
                    if ($i = !$i) {
0 ignored issues
show
introduced by
The condition $i is always false.
Loading history...
3262
                        $openCount += substr_count($value, '{');
3263
                        $closeCount += substr_count($value, '}');
3264
                        $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
3265
                    }
3266
                }
3267
                unset($value);
3268
                //    Then rebuild the formula string
3269
                $formula = implode('"', $temp);
3270
            } else {
3271
                //    If there's no quoted strings, then we do a simple count/replace
3272
                $openCount = substr_count($formula, '{');
3273
                $closeCount = substr_count($formula, '}');
3274
                $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
3275
            }
3276
            //    Trap for mismatched braces and trigger an appropriate error
3277
            if ($openCount < $closeCount) {
3278
                if ($openCount > 0) {
3279
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->raiseFormu...hed matrix braces '}'') returns the type false which is incompatible with the documented return type string.
Loading history...
3280
                }
3281
3282
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->raiseFormu...ected '}' encountered') returns the type false which is incompatible with the documented return type string.
Loading history...
3283
            } elseif ($openCount > $closeCount) {
3284
                if ($closeCount > 0) {
3285
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->raiseFormu...hed matrix braces '{'') returns the type false which is incompatible with the documented return type string.
Loading history...
3286
                }
3287
3288
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->raiseFormu...ected '{' encountered') returns the type false which is incompatible with the documented return type string.
Loading history...
3289
            }
3290
        }
3291
3292 167
        return $formula;
3293
    }
3294
3295
    private static function mkMatrix(...$args)
3296
    {
3297
        return $args;
3298
    }
3299
3300
    //    Binary Operators
3301
    //    These operators always work on two values
3302
    //    Array key is the operator, the value indicates whether this is a left or right associative operator
3303
    private static $operatorAssociativity = [
3304
        '^' => 0, //    Exponentiation
3305
        '*' => 0, '/' => 0, //    Multiplication and Division
3306
        '+' => 0, '-' => 0, //    Addition and Subtraction
3307
        '&' => 0, //    Concatenation
3308
        '|' => 0, ':' => 0, //    Intersect and Range
3309
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3310
    ];
3311
3312
    //    Comparison (Boolean) Operators
3313
    //    These operators work on two values, but always return a boolean result
3314
    private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
3315
3316
    //    Operator Precedence
3317
    //    This list includes all valid operators, whether binary (including boolean) or unary (such as %)
3318
    //    Array key is the operator, the value is its precedence
3319
    private static $operatorPrecedence = [
3320
        ':' => 8, //    Range
3321
        '|' => 7, //    Intersect
3322
        '~' => 6, //    Negation
3323
        '%' => 5, //    Percentage
3324
        '^' => 4, //    Exponentiation
3325
        '*' => 3, '/' => 3, //    Multiplication and Division
3326
        '+' => 2, '-' => 2, //    Addition and Subtraction
3327
        '&' => 1, //    Concatenation
3328
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3329
    ];
3330
3331
    // Convert infix to postfix notation
3332
3333
    /**
3334
     * @param string $formula
3335
     * @param null|\PhpOffice\PhpSpreadsheet\Cell\Cell $pCell
3336
     *
3337
     * @return bool
3338
     */
3339 167
    private function _parseFormula($formula, Cell $pCell = null)
3340
    {
3341 167
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
0 ignored issues
show
introduced by
The condition $formula = $this->conver...im($formula)) === false is always false.
Loading history...
3342
            return false;
3343
        }
3344
3345
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
3346
        //        so we store the parent worksheet so that we can re-attach it when necessary
3347 167
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3348
3349 167
        $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION .
3350 167
                                '|' . self::CALCULATION_REGEXP_CELLREF .
3351 167
                                '|' . self::CALCULATION_REGEXP_NUMBER .
3352 167
                                '|' . self::CALCULATION_REGEXP_STRING .
3353 167
                                '|' . self::CALCULATION_REGEXP_OPENBRACE .
3354 167
                                '|' . self::CALCULATION_REGEXP_NAMEDRANGE .
3355 167
                                '|' . self::CALCULATION_REGEXP_ERROR .
3356 167
                                ')/si';
3357
3358
        //    Start with initialisation
3359 167
        $index = 0;
3360 167
        $stack = new Stack();
3361 167
        $output = [];
3362 167
        $expectingOperator = false; //    We use this test in syntax-checking the expression to determine when a
3363
                                                    //        - is a negation or + is a positive operator rather than an operation
3364 167
        $expectingOperand = false; //    We use this test in syntax-checking the expression to determine whether an operand
3365
                                                    //        should be null in a function call
3366
3367
        // IF branch pruning
3368
        // currently pending storeKey (last item of the storeKeysStack
3369 167
        $pendingStoreKey = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $pendingStoreKey is dead and can be removed.
Loading history...
3370
        // stores a list of storeKeys (string[])
3371 167
        $pendingStoreKeysStack = [];
3372 167
        $expectingConditionMap = []; // ['storeKey' => true, ...]
3373 167
        $expectingThenMap = []; // ['storeKey' => true, ...]
3374 167
        $expectingElseMap = []; // ['storeKey' => true, ...]
3375 167
        $parenthesisDepthMap = []; // ['storeKey' => 4, ...]
3376
3377
        //    The guts of the lexical parser
3378
        //    Loop through the formula extracting each operator and operand in turn
3379 167
        while (true) {
3380
            // Branch pruning: we adapt the output item to the context (it will
3381
            // be used to limit its computation)
3382 167
            $currentCondition = null;
3383 167
            $currentOnlyIf = null;
3384 167
            $currentOnlyIfNot = null;
3385 167
            $previousStoreKey = null;
3386 167
            $pendingStoreKey = end($pendingStoreKeysStack);
3387
3388 167
            if ($this->branchPruningEnabled) {
3389
                // this is a condition ?
3390 167
                if (isset($expectingConditionMap[$pendingStoreKey]) && $expectingConditionMap[$pendingStoreKey]) {
3391 25
                    $currentCondition = $pendingStoreKey;
3392 25
                    $stackDepth = count($pendingStoreKeysStack);
3393 25
                    if ($stackDepth > 1) { // nested if
3394 7
                        $previousStoreKey = $pendingStoreKeysStack[$stackDepth - 2];
3395
                    }
3396
                }
3397 167
                if (isset($expectingThenMap[$pendingStoreKey]) && $expectingThenMap[$pendingStoreKey]) {
3398 25
                    $currentOnlyIf = $pendingStoreKey;
3399
                } elseif (isset($previousStoreKey)) {
3400 7
                    if (isset($expectingThenMap[$previousStoreKey]) && $expectingThenMap[$previousStoreKey]) {
3401 2
                        $currentOnlyIf = $previousStoreKey;
3402
                    }
3403
                }
3404 167
                if (isset($expectingElseMap[$pendingStoreKey]) && $expectingElseMap[$pendingStoreKey]) {
3405 22
                    $currentOnlyIfNot = $pendingStoreKey;
3406
                } elseif (isset($previousStoreKey)) {
3407 7
                    if (isset($expectingElseMap[$previousStoreKey]) && $expectingElseMap[$previousStoreKey]) {
3408 5
                        $currentOnlyIfNot = $previousStoreKey;
3409
                    }
3410
                }
3411
            }
3412
3413 167
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
3414 167
            if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
3415 39
                $opCharacter .= $formula[++$index];
3416
            }
3417
3418
            //    Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
3419 167
            $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
3420
3421 167
            if ($opCharacter == '-' && !$expectingOperator) {                //    Is it a negation instead of a minus?
3422
                //    Put a negation on the stack
3423 3
                $stack->push('Unary Operator', '~', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3424 3
                ++$index; //        and drop the negation symbol
3425 167
            } elseif ($opCharacter == '%' && $expectingOperator) {
3426
                //    Put a percentage on the stack
3427
                $stack->push('Unary Operator', '%', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3428
                ++$index;
3429 167
            } elseif ($opCharacter == '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
3430
                ++$index; //    Drop the redundant plus symbol
3431 167
            } elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) {    //    We have to explicitly deny a tilde or pipe, because they are legal
3432
                return $this->raiseFormulaError("Formula Error: Illegal character '~'"); //        on the stack but not in the input expression
3433 167
            } elseif ((isset(self::$operators[$opCharacter]) or $isOperandOrFunction) && $expectingOperator) {    //    Are we putting an operator on the stack?
3434 157
                while ($stack->count() > 0 &&
3435 157
                    ($o2 = $stack->last()) &&
3436 157
                    isset(self::$operators[$o2['value']]) &&
3437 157
                    @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3438 3
                    $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3439
                }
3440
3441
                //    Finally put our current operator onto the stack
3442 157
                $stack->push('Binary Operator', $opCharacter, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3443
3444 157
                ++$index;
3445 157
                $expectingOperator = false;
3446 167
            } elseif ($opCharacter == ')' && $expectingOperator) {            //    Are we expecting to close a parenthesis?
3447 63
                $expectingOperand = false;
3448 63
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3449 49
                    if ($o2 === null) {
3450
                        return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"');
3451
                    }
3452 49
                    $output[] = $o2;
3453
                }
3454 63
                $d = $stack->last(2);
3455
3456
                // Branch pruning we decrease the depth whether is it a function
3457
                // call or a parenthesis
3458 63
                if (!empty($pendingStoreKey)) {
3459 25
                    $parenthesisDepthMap[$pendingStoreKey] -= 1;
3460
                }
3461
3462 63
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {    //    Did this parenthesis just close a function?
3463 63
                    if (!empty($pendingStoreKey) && $parenthesisDepthMap[$pendingStoreKey] == -1) {
3464
                        // we are closing an IF(
3465 25
                        if ($d['value'] != 'IF(') {
3466
                            return $this->raiseFormulaError('Parser bug we should be in an "IF("');
3467
                        }
3468 25
                        if ($expectingConditionMap[$pendingStoreKey]) {
3469
                            return $this->raiseFormulaError('We should not be expecting a condition');
3470
                        }
3471 25
                        $expectingThenMap[$pendingStoreKey] = false;
3472 25
                        $expectingElseMap[$pendingStoreKey] = false;
3473 25
                        $parenthesisDepthMap[$pendingStoreKey] -= 1;
3474 25
                        array_pop($pendingStoreKeysStack);
3475 25
                        unset($pendingStoreKey);
3476
                    }
3477
3478 63
                    $functionName = $matches[1]; //    Get the function name
3479 63
                    $d = $stack->pop();
3480 63
                    $argumentCount = $d['value']; //    See how many arguments there were (argument count is the next value stored on the stack)
3481 63
                    $output[] = $d; //    Dump the argument count on the output
3482 63
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
3483 63
                    if (isset(self::$controlFunctions[$functionName])) {
3484
                        $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
3485
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
0 ignored issues
show
Unused Code introduced by
The assignment to $functionCall is dead and can be removed.
Loading history...
3486 63
                    } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3487 63
                        $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
3488 63
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3489
                    } else {    // did we somehow push a non-function on the stack? this should never happen
3490
                        return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack');
3491
                    }
3492
                    //    Check the argument count
3493 63
                    $argumentCountError = false;
3494 63
                    if (is_numeric($expectedArgumentCount)) {
3495 31
                        if ($expectedArgumentCount < 0) {
3496 1
                            if ($argumentCount > abs($expectedArgumentCount)) {
3497
                                $argumentCountError = true;
3498 1
                                $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
3499
                            }
3500
                        } else {
3501 30
                            if ($argumentCount != $expectedArgumentCount) {
3502
                                $argumentCountError = true;
3503 31
                                $expectedArgumentCountString = $expectedArgumentCount;
3504
                            }
3505
                        }
3506 55
                    } elseif ($expectedArgumentCount != '*') {
3507 55
                        $isOperandOrFunction = preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch);
0 ignored issues
show
Unused Code introduced by
The assignment to $isOperandOrFunction is dead and can be removed.
Loading history...
3508 55
                        switch ($argMatch[2]) {
3509 55
                            case '+':
3510 45
                                if ($argumentCount < $argMatch[1]) {
3511
                                    $argumentCountError = true;
3512
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
3513
                                }
3514
3515 45
                                break;
3516 30
                            case '-':
3517 26
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
3518
                                    $argumentCountError = true;
3519
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
3520
                                }
3521
3522 26
                                break;
3523 4
                            case ',':
3524 4
                                if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
3525
                                    $argumentCountError = true;
3526
                                    $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
3527
                                }
3528
3529 4
                                break;
3530
                        }
3531
                    }
3532 63
                    if ($argumentCountError) {
3533
                        return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $expectedArgumentCountString does not seem to be defined for all execution paths leading up to this point.
Loading history...
3534
                    }
3535
                }
3536 63
                ++$index;
3537 167
            } elseif ($opCharacter == ',') {            //    Is this the separator for function arguments?
3538 44
                if (!empty($pendingStoreKey) &&
3539 44
                    $parenthesisDepthMap[$pendingStoreKey] == 0
3540
                ) {
3541
                    // We must go to the IF next argument
3542 25
                    if ($expectingConditionMap[$pendingStoreKey]) {
3543 25
                        $expectingConditionMap[$pendingStoreKey] = false;
3544 25
                        $expectingThenMap[$pendingStoreKey] = true;
3545 22
                    } elseif ($expectingThenMap[$pendingStoreKey]) {
3546 22
                        $expectingThenMap[$pendingStoreKey] = false;
3547 22
                        $expectingElseMap[$pendingStoreKey] = true;
3548
                    } elseif ($expectingElseMap[$pendingStoreKey]) {
3549
                        return $this->raiseFormulaError('Reaching fourth argument of an IF');
3550
                    }
3551
                }
3552 44
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3553 37
                    if ($o2 === null) {
3554
                        return $this->raiseFormulaError('Formula Error: Unexpected ,');
3555
                    }
3556 37
                    $output[] = $o2; // pop the argument expression stuff and push onto the output
3557
                }
3558
                //    If we've a comma when we're expecting an operand, then what we actually have is a null operand;
3559
                //        so push a null onto the stack
3560 44
                if (($expectingOperand) || (!$expectingOperator)) {
3561
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3562
                }
3563
                // make sure there was a function
3564 44
                $d = $stack->last(2);
3565 44
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {
3566
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
3567
                }
3568 44
                $d = $stack->pop();
3569 44
                $itemStoreKey = $d['storeKey'] ?? null;
3570 44
                $itemOnlyIf = $d['onlyIf'] ?? null;
3571 44
                $itemOnlyIfNot = $d['onlyIfNot'] ?? null;
3572 44
                $stack->push($d['type'], ++$d['value'], $d['reference'], $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // increment the argument count
3573 44
                $stack->push('Brace', '(', null, $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // put the ( back on, we'll need to pop back to it again
3574 44
                $expectingOperator = false;
3575 44
                $expectingOperand = true;
3576 44
                ++$index;
3577 167
            } elseif ($opCharacter == '(' && !$expectingOperator) {
3578 3
                if (!empty($pendingStoreKey)) { // Branch pruning: we go deeper
3579
                    $parenthesisDepthMap[$pendingStoreKey] += 1;
3580
                }
3581 3
                $stack->push('Brace', '(', null, $currentCondition, $currentOnlyIf, $currentOnlyIf);
3582 3
                ++$index;
3583 167
            } elseif ($isOperandOrFunction && !$expectingOperator) {    // do we now have a function/variable/number?
3584 167
                $expectingOperator = true;
3585 167
                $expectingOperand = false;
3586 167
                $val = $match[1];
3587 167
                $length = strlen($val);
3588
3589 167
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
3590 63
                    $val = preg_replace('/\s/u', '', $val);
3591 63
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
3592 63
                        $valToUpper = strtoupper($val);
3593
                        // here $matches[1] will contain values like "IF"
3594
                        // and $val "IF("
3595 63
                        if ($this->branchPruningEnabled && ($valToUpper == 'IF(')) { // we handle a new if
3596 25
                            $pendingStoreKey = $this->getUnusedBranchStoreKey();
3597 25
                            $pendingStoreKeysStack[] = $pendingStoreKey;
3598 25
                            $expectingConditionMap[$pendingStoreKey] = true;
3599 25
                            $parenthesisDepthMap[$pendingStoreKey] = 0;
3600
                        } else { // this is not a if but we good deeper
3601 56
                            if (!empty($pendingStoreKey) && array_key_exists($pendingStoreKey, $parenthesisDepthMap)) {
3602 7
                                $parenthesisDepthMap[$pendingStoreKey] += 1;
3603
                            }
3604
                        }
3605
3606 63
                        $stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3607
                        // tests if the function is closed right after opening
3608 63
                        $ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
3609 63
                        if ($ax) {
3610 12
                            $stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3611 12
                            $expectingOperator = true;
3612
                        } else {
3613 62
                            $stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3614 62
                            $expectingOperator = false;
3615
                        }
3616 63
                        $stack->push('Brace', '(');
3617
                    } else {    // it's a var w/ implicit multiplication
3618 63
                        $output[] = ['type' => 'Value', 'value' => $matches[1], 'reference' => null];
3619
                    }
3620 166
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
3621
                    //    Watch for this case-change when modifying to allow cell references in different worksheets...
3622
                    //    Should only be applied to the actual cell column, not the worksheet name
3623
3624
                    //    If the last entry on the stack was a : operator, then we have a cell range reference
3625 76
                    $testPrevOp = $stack->last(1);
3626 76
                    if ($testPrevOp['value'] == ':') {
3627
                        //    If we have a worksheet reference, then we're playing with a 3D reference
3628 59
                        if ($matches[2] == '') {
3629
                            //    Otherwise, we 'inherit' the worksheet reference from the start cell reference
3630
                            //    The start of the cell range reference should be the last entry in $output
3631 59
                            $startCellRef = $output[count($output) - 1]['value'];
3632 59
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $startCellRef, $startMatches);
3633 59
                            if ($startMatches[2] > '') {
3634 59
                                $val = $startMatches[2] . '!' . $val;
3635
                            }
3636
                        } else {
3637
                            return $this->raiseFormulaError('3D Range references are not yet supported');
3638
                        }
3639
                    }
3640
3641 76
                    $outputItem = $stack->getStackItem('Cell Reference', $val, $val, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3642
3643 76
                    $output[] = $outputItem;
3644
                } else {    // it's a variable, constant, string, number or boolean
3645
                    //    If the last entry on the stack was a : operator, then we may have a row or column range reference
3646 139
                    $testPrevOp = $stack->last(1);
3647 139
                    if ($testPrevOp['value'] == ':') {
3648 1
                        $startRowColRef = $output[count($output) - 1]['value'];
3649 1
                        [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
3650 1
                        if ($rangeWS1 != '') {
3651
                            $rangeWS1 .= '!';
3652
                        }
3653 1
                        [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
3654 1
                        if ($rangeWS2 != '') {
3655
                            $rangeWS2 .= '!';
3656
                        } else {
3657 1
                            $rangeWS2 = $rangeWS1;
3658
                        }
3659 1
                        if ((is_int($startRowColRef)) && (ctype_digit($val)) &&
3660 1
                            ($startRowColRef <= 1048576) && ($val <= 1048576)) {
3661
                            //    Row range
3662
                            $endRowColRef = ($pCellParent !== null) ? $pCellParent->getHighestColumn() : 'XFD'; //    Max 16,384 columns for Excel2007
3663
                            $output[count($output) - 1]['value'] = $rangeWS1 . 'A' . $startRowColRef;
3664
                            $val = $rangeWS2 . $endRowColRef . $val;
3665 1
                        } elseif ((ctype_alpha($startRowColRef)) && (ctype_alpha($val)) &&
3666 1
                            (strlen($startRowColRef) <= 3) && (strlen($val) <= 3)) {
3667
                            //    Column range
3668
                            $endRowColRef = ($pCellParent !== null) ? $pCellParent->getHighestRow() : 1048576; //    Max 1,048,576 rows for Excel2007
3669
                            $output[count($output) - 1]['value'] = $rangeWS1 . strtoupper($startRowColRef) . '1';
3670
                            $val = $rangeWS2 . $val . $endRowColRef;
3671
                        }
3672
                    }
3673
3674 139
                    $localeConstant = false;
3675 139
                    if ($opCharacter == '"') {
3676
                        //    UnEscape any quotes within the string
3677 83
                        $val = self::wrapResult(str_replace('""', '"', self::unwrapResult($val)));
3678 90
                    } elseif (is_numeric($val)) {
3679 88
                        if ((strpos($val, '.') !== false) || (stripos($val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
3680 20
                            $val = (float) $val;
3681
                        } else {
3682 88
                            $val = (int) $val;
3683
                        }
3684 6
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
3685 1
                        $excelConstant = trim(strtoupper($val));
3686 1
                        $val = self::$excelConstants[$excelConstant];
3687 5
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
3688
                        $val = self::$excelConstants[$localeConstant];
3689
                    }
3690 139
                    $details = $stack->getStackItem('Value', $val, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3691 139
                    if ($localeConstant) {
3692
                        $details['localeValue'] = $localeConstant;
3693
                    }
3694 139
                    $output[] = $details;
3695
                }
3696 167
                $index += $length;
3697
            } elseif ($opCharacter == '$') {    // absolute row or column range
3698
                ++$index;
3699
            } elseif ($opCharacter == ')') {    // miscellaneous error checking
3700
                if ($expectingOperand) {
3701
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3702
                    $expectingOperand = false;
3703
                    $expectingOperator = true;
3704
                } else {
3705
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
3706
                }
3707
            } elseif (isset(self::$operators[$opCharacter]) && !$expectingOperator) {
3708
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
3709
            } else {    // I don't even want to know what you did to get here
3710
                return $this->raiseFormulaError('Formula Error: An unexpected error occured');
3711
            }
3712
            //    Test for end of formula string
3713 167
            if ($index == strlen($formula)) {
3714
                //    Did we end with an operator?.
3715
                //    Only valid for the % unary operator
3716 167
                if ((isset(self::$operators[$opCharacter])) && ($opCharacter != '%')) {
3717
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
3718
                }
3719
3720 167
                break;
3721
            }
3722
            //    Ignore white space
3723 165
            while (($formula[$index] == "\n") || ($formula[$index] == "\r")) {
3724
                ++$index;
3725
            }
3726 165
            if ($formula[$index] == ' ') {
3727 64
                while ($formula[$index] == ' ') {
3728 64
                    ++$index;
3729
                }
3730
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
3731
                //        Cell References) then we have an INTERSECTION operator
3732 64
                if (($expectingOperator) && (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
3733 64
                    ($output[count($output) - 1]['type'] == 'Cell Reference')) {
3734
                    while ($stack->count() > 0 &&
3735
                        ($o2 = $stack->last()) &&
3736
                        isset(self::$operators[$o2['value']]) &&
3737
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3738
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3739
                    }
3740
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
3741
                    $expectingOperator = false;
3742
                }
3743
            }
3744
        }
3745
3746 167
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
3747 131
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
3748
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
3749
            }
3750 131
            $output[] = $op;
3751
        }
3752
3753 167
        return $output;
3754
    }
3755
3756 152
    private static function dataTestReference(&$operandData)
3757
    {
3758 152
        $operand = $operandData['value'];
3759 152
        if (($operandData['reference'] === null) && (is_array($operand))) {
3760 1
            $rKeys = array_keys($operand);
3761 1
            $rowKey = array_shift($rKeys);
3762 1
            $cKeys = array_keys(array_keys($operand[$rowKey]));
3763 1
            $colKey = array_shift($cKeys);
3764 1
            if (ctype_upper($colKey)) {
3765
                $operandData['reference'] = $colKey . $rowKey;
3766
            }
3767
        }
3768
3769 152
        return $operand;
3770
    }
3771
3772
    // evaluate postfix notation
3773
3774
    /**
3775
     * @param mixed $tokens
3776
     * @param null|string $cellID
3777
     * @param null|Cell $pCell
3778
     *
3779
     * @return bool
3780
     */
3781 162
    private function processTokenStack($tokens, $cellID = null, Cell $pCell = null)
3782
    {
3783 162
        if ($tokens == false) {
3784
            return false;
3785
        }
3786
3787
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
3788
        //        so we store the parent cell collection so that we can re-attach it when necessary
3789 162
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
3790 162
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
3791 162
        $stack = new Stack();
3792
3793
        // Stores branches that have been pruned
3794 162
        $fakedForBranchPruning = [];
3795
        // help us to know when pruning ['branchTestId' => true/false]
3796 162
        $branchStore = [];
3797
3798
        //    Loop through each token in turn
3799 162
        foreach ($tokens as $tokenData) {
3800 162
            $token = $tokenData['value'];
3801
3802
            // Branch pruning: skip useless resolutions
3803 162
            $storeKey = $tokenData['storeKey'] ?? null;
3804 162
            if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) {
3805 21
                $onlyIfStoreKey = $tokenData['onlyIf'];
3806 21
                $storeValue = $branchStore[$onlyIfStoreKey] ?? null;
3807 21
                if (is_array($storeValue)) {
3808 18
                    $wrappedItem = end($storeValue);
3809 18
                    $storeValue = end($wrappedItem);
3810
                }
3811
3812 21
                if (isset($storeValue) && (($storeValue !== true)
3813 21
                    || ($storeValue === 'Pruned branch'))
3814
                ) {
3815
                    // If branching value is not true, we don't need to compute
3816 17
                    if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) {
3817 16
                        $stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token);
3818 16
                        $fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true;
3819
                    }
3820
3821 17
                    if (isset($storeKey)) {
3822
                        // We are processing an if condition
3823
                        // We cascade the pruning to the depending branches
3824 1
                        $branchStore[$storeKey] = 'Pruned branch';
3825 1
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
3826 1
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
3827
                    }
3828
3829 17
                    continue;
3830
                }
3831
            }
3832
3833 162
            if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) {
3834 20
                $onlyIfNotStoreKey = $tokenData['onlyIfNot'];
3835 20
                $storeValue = $branchStore[$onlyIfNotStoreKey] ?? null;
3836 20
                if (is_array($storeValue)) {
3837 17
                    $wrappedItem = end($storeValue);
3838 17
                    $storeValue = end($wrappedItem);
3839
                }
3840 20
                if (isset($storeValue) && ($storeValue
3841 20
                    || ($storeValue === 'Pruned branch'))
3842
                ) {
3843
                    // If branching value is true, we don't need to compute
3844 18
                    if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) {
3845 18
                        $stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token);
3846 18
                        $fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true;
3847
                    }
3848
3849 18
                    if (isset($storeKey)) {
3850
                        // We are processing an if condition
3851
                        // We cascade the pruning to the depending branches
3852 2
                        $branchStore[$storeKey] = 'Pruned branch';
3853 2
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
3854 2
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
3855
                    }
3856
3857 18
                    continue;
3858
                }
3859
            }
3860
3861
            // if the token is a binary operator, pop the top two values off the stack, do the operation, and push the result back on the stack
3862 162
            if (isset(self::$binaryOperators[$token])) {
3863
                //    We must have two operands, error if we don't
3864 152
                if (($operand2Data = $stack->pop()) === null) {
3865
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3866
                }
3867 152
                if (($operand1Data = $stack->pop()) === null) {
3868
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3869
                }
3870
3871 152
                $operand1 = self::dataTestReference($operand1Data);
3872 152
                $operand2 = self::dataTestReference($operand2Data);
3873
3874
                //    Log what we're doing
3875 152
                if ($token == ':') {
3876 55
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
3877
                } else {
3878 133
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
3879
                }
3880
3881
                //    Process the operation in the appropriate manner
3882 152
                switch ($token) {
3883
                    //    Comparison (Boolean) Operators
3884 152
                    case '>':            //    Greater than
3885 136
                    case '<':            //    Less than
3886 124
                    case '>=':            //    Greater than or Equal to
3887 115
                    case '<=':            //    Less than or Equal to
3888 104
                    case '=':            //    Equality
3889 75
                    case '<>':            //    Inequality
3890 111
                        $result = $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
3891 111
                        if (isset($storeKey)) {
3892 19
                            $branchStore[$storeKey] = $result;
3893
                        }
3894
3895 111
                        break;
3896
                    //    Binary Operators
3897 67
                    case ':':            //    Range
3898 55
                        if (strpos($operand1Data['reference'], '!') !== false) {
3899 55
                            [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
3900
                        } else {
3901 1
                            $sheet1 = ($pCellParent !== null) ? $pCellWorksheet->getTitle() : '';
1 ignored issue
show
Bug introduced by
The method getTitle() does not exist on null. ( Ignorable by Annotation )

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

3901
                            $sheet1 = ($pCellParent !== null) ? $pCellWorksheet->/** @scrutinizer ignore-call */ getTitle() : '';

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
3902
                        }
3903
3904 55
                        [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true);
3905 55
                        if (empty($sheet2)) {
3906 10
                            $sheet2 = $sheet1;
3907
                        }
3908
3909 55
                        if ($sheet1 == $sheet2) {
3910 55
                            if ($operand1Data['reference'] === null) {
3911
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
3912
                                    $operand1Data['reference'] = $pCell->getColumn() . $operand1Data['value'];
1 ignored issue
show
Bug introduced by
The method getColumn() does not exist on null. ( Ignorable by Annotation )

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

3912
                                    $operand1Data['reference'] = $pCell->/** @scrutinizer ignore-call */ getColumn() . $operand1Data['value'];

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
3913
                                } elseif (trim($operand1Data['reference']) == '') {
3914
                                    $operand1Data['reference'] = $pCell->getCoordinate();
3915
                                } else {
3916
                                    $operand1Data['reference'] = $operand1Data['value'] . $pCell->getRow();
3917
                                }
3918
                            }
3919 55
                            if ($operand2Data['reference'] === null) {
3920
                                if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
3921
                                    $operand2Data['reference'] = $pCell->getColumn() . $operand2Data['value'];
3922
                                } elseif (trim($operand2Data['reference']) == '') {
3923
                                    $operand2Data['reference'] = $pCell->getCoordinate();
3924
                                } else {
3925
                                    $operand2Data['reference'] = $operand2Data['value'] . $pCell->getRow();
3926
                                }
3927
                            }
3928
3929 55
                            $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
3930 55
                            $oCol = $oRow = [];
3931 55
                            foreach ($oData as $oDatum) {
3932 55
                                $oCR = Coordinate::coordinateFromString($oDatum);
3933 55
                                $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
3934 55
                                $oRow[] = $oCR[1];
3935
                            }
3936 55
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
3937 55
                            if ($pCellParent !== null) {
3938 55
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
3939
                            } else {
3940
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3941
                            }
3942 55
                            $stack->push('Cell Reference', $cellValue, $cellRef);
3943
                        } else {
3944
                            $stack->push('Error', Functions::REF(), null);
3945
                        }
3946
3947 55
                        break;
3948 36
                    case '+':            //    Addition
3949 26
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
3950 26
                        if (isset($storeKey)) {
3951 1
                            $branchStore[$storeKey] = $result;
3952
                        }
3953
3954 26
                        break;
3955 28
                    case '-':            //    Subtraction
3956 7
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
3957 7
                        if (isset($storeKey)) {
3958
                            $branchStore[$storeKey] = $result;
3959
                        }
3960
3961 7
                        break;
3962 26
                    case '*':            //    Multiplication
3963 18
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
3964 18
                        if (isset($storeKey)) {
3965
                            $branchStore[$storeKey] = $result;
3966
                        }
3967
3968 18
                        break;
3969 12
                    case '/':            //    Division
3970 8
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
3971 8
                        if (isset($storeKey)) {
3972
                            $branchStore[$storeKey] = $result;
3973
                        }
3974
3975 8
                        break;
3976 7
                    case '^':            //    Exponential
3977
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
3978
                        if (isset($storeKey)) {
3979
                            $branchStore[$storeKey] = $result;
3980
                        }
3981
3982
                        break;
3983 7
                    case '&':            //    Concatenation
3984
                        //    If either of the operands is a matrix, we need to treat them both as matrices
3985
                        //        (converting the other operand to a matrix if need be); then perform the required
3986
                        //        matrix operation
3987 7
                        if (is_bool($operand1)) {
3988
                            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3989
                        }
3990 7
                        if (is_bool($operand2)) {
3991
                            $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3992
                        }
3993 7
                        if ((is_array($operand1)) || (is_array($operand2))) {
3994
                            //    Ensure that both operands are arrays/matrices
3995 5
                            self::checkMatrixOperands($operand1, $operand2, 2);
3996
3997
                            try {
3998
                                //    Convert operand 1 from a PHP array to a matrix
3999 5
                                $matrix = new Shared\JAMA\Matrix($operand1);
4000
                                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4001 5
                                $matrixResult = $matrix->concat($operand2);
4002 5
                                $result = $matrixResult->getArray();
4003
                            } catch (\Exception $ex) {
4004
                                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4005 5
                                $result = '#VALUE!';
4006
                            }
4007
                        } else {
4008 2
                            $result = '"' . str_replace('""', '"', self::unwrapResult($operand1) . self::unwrapResult($operand2)) . '"';
4009
                        }
4010 7
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
4011 7
                        $stack->push('Value', $result);
4012
4013 7
                        if (isset($storeKey)) {
4014
                            $branchStore[$storeKey] = $result;
4015
                        }
4016
4017 7
                        break;
4018
                    case '|':            //    Intersect
4019
                        $rowIntersect = array_intersect_key($operand1, $operand2);
4020
                        $cellIntersect = $oCol = $oRow = [];
4021
                        foreach (array_keys($rowIntersect) as $row) {
4022
                            $oRow[] = $row;
4023
                            foreach ($rowIntersect[$row] as $col => $data) {
4024
                                $oCol[] = Coordinate::columnIndexFromString($col) - 1;
4025
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
4026
                            }
4027
                        }
4028
                        $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4029
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
4030
                        $stack->push('Value', $cellIntersect, $cellRef);
4031
4032 152
                        break;
4033
                }
4034
4035
                // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
4036 162
            } elseif (($token === '~') || ($token === '%')) {
4037 3
                if (($arg = $stack->pop()) === null) {
4038
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4039
                }
4040 3
                $arg = $arg['value'];
4041 3
                if ($token === '~') {
4042 3
                    $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg));
4043 3
                    $multiplier = -1;
4044
                } else {
4045
                    $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg));
4046
                    $multiplier = 0.01;
4047
                }
4048 3
                if (is_array($arg)) {
4049
                    self::checkMatrixOperands($arg, $multiplier, 2);
4050
4051
                    try {
4052
                        $matrix1 = new Shared\JAMA\Matrix($arg);
4053
                        $matrixResult = $matrix1->arrayTimesEquals($multiplier);
4054
                        $result = $matrixResult->getArray();
4055
                    } catch (\Exception $ex) {
4056
                        $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4057
                        $result = '#VALUE!';
4058
                    }
4059
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
4060
                    $stack->push('Value', $result);
4061
                    if (isset($storeKey)) {
4062
                        $branchStore[$storeKey] = $result;
4063
                    }
4064
                } else {
4065 3
                    $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack);
4066
                }
4067 162
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token, $matches)) {
4068 71
                $cellRef = null;
4069 71
                if (isset($matches[8])) {
4070
                    if ($pCell === null) {
4071
                        //                        We can't access the range, so return a REF error
4072
                        $cellValue = Functions::REF();
4073
                    } else {
4074
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
4075
                        if ($matches[2] > '') {
4076
                            $matches[2] = trim($matches[2], "\"'");
4077
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
4078
                                //    It's a Reference to an external spreadsheet (not currently supported)
4079
                                return $this->raiseFormulaError('Unable to access External Workbook');
4080
                            }
4081
                            $matches[2] = trim($matches[2], "\"'");
4082
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]);
4083
                            if ($pCellParent !== null) {
4084
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4085
                            } else {
4086
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4087
                            }
4088
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
4089
                        } else {
4090
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet');
4091
                            if ($pCellParent !== null) {
4092
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4093
                            } else {
4094
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4095
                            }
4096
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
4097
                        }
4098
                    }
4099
                } else {
4100 71
                    if ($pCell === null) {
4101
                        //                        We can't access the cell, so return a REF error
4102
                        $cellValue = Functions::REF();
4103
                    } else {
4104 71
                        $cellRef = $matches[6] . $matches[7];
4105 71
                        if ($matches[2] > '') {
4106 15
                            $matches[2] = trim($matches[2], "\"'");
4107 15
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
4108
                                //    It's a Reference to an external spreadsheet (not currently supported)
4109
                                return $this->raiseFormulaError('Unable to access External Workbook');
4110
                            }
4111 15
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]);
4112 15
                            if ($pCellParent !== null) {
4113 15
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
4114 15
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
4115 15
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4116 15
                                    $pCell->attach($pCellParent);
4117
                                } else {
4118 15
                                    $cellValue = null;
4119
                                }
4120
                            } else {
4121
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4122
                            }
4123 15
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
4124
                        } else {
4125 58
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet');
4126 58
                            if ($pCellParent->has($cellRef)) {
1 ignored issue
show
Bug introduced by
The method has() does not exist on null. ( Ignorable by Annotation )

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

4126
                            if ($pCellParent->/** @scrutinizer ignore-call */ has($cellRef)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
4127 57
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4128 57
                                $pCell->attach($pCellParent);
1 ignored issue
show
Bug introduced by
It seems like $pCellParent can also be of type null; however, parameter $parent of PhpOffice\PhpSpreadsheet\Cell\Cell::attach() does only seem to accept PhpOffice\PhpSpreadsheet\Collection\Cells, 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

4128
                                $pCell->attach(/** @scrutinizer ignore-type */ $pCellParent);
Loading history...
4129
                            } else {
4130 3
                                $cellValue = null;
4131
                            }
4132 58
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
4133
                        }
4134
                    }
4135
                }
4136 71
                $stack->push('Value', $cellValue, $cellRef);
4137 71
                if (isset($storeKey)) {
4138 71
                    $branchStore[$storeKey] = $cellValue;
4139
                }
4140
4141
                // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
4142 147
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) {
4143 58
                if ($pCellParent) {
4144 58
                    $pCell->attach($pCellParent);
4145
                }
4146 58
                if (($cellID == 'AC99') || (isset($pCell) && $pCell->getCoordinate() == 'AC99')) {
4147
                    if (defined('RESOLVING')) {
4148
                        define('RESOLVING2', true);
4149
                    } else {
4150
                        define('RESOLVING', true);
4151
                    }
4152
                }
4153
4154 58
                $functionName = $matches[1];
4155 58
                $argCount = $stack->pop();
4156 58
                $argCount = $argCount['value'];
4157 58
                if ($functionName != 'MKMATRIX') {
4158 58
                    $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's'));
4159
                }
4160 58
                if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) {    // function
4161 58
                    if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
4162 58
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
4163 58
                        $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
4164 58
                        $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
4165
                    } elseif (isset(self::$controlFunctions[$functionName])) {
4166
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
4167
                        $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
4168
                        $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
4169
                    }
4170
                    // get the arguments for this function
4171 58
                    $args = $argArrayVals = [];
4172 58
                    for ($i = 0; $i < $argCount; ++$i) {
4173 57
                        $arg = $stack->pop();
4174 57
                        $a = $argCount - $i - 1;
4175 57
                        if (($passByReference) &&
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $passByReference does not seem to be defined for all execution paths leading up to this point.
Loading history...
4176 57
                            (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) &&
4177 57
                            (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) {
4178 2
                            if ($arg['reference'] === null) {
4179
                                $args[] = $cellID;
4180
                                if ($functionName != 'MKMATRIX') {
4181
                                    $argArrayVals[] = $this->showValue($cellID);
4182
                                }
4183
                            } else {
4184 2
                                $args[] = $arg['reference'];
4185 2
                                if ($functionName != 'MKMATRIX') {
4186 2
                                    $argArrayVals[] = $this->showValue($arg['reference']);
4187
                                }
4188
                            }
4189
                        } else {
4190 56
                            $args[] = self::unwrapResult($arg['value']);
4191 56
                            if ($functionName != 'MKMATRIX') {
4192 56
                                $argArrayVals[] = $this->showValue($arg['value']);
4193
                            }
4194
                        }
4195
                    }
4196
4197
                    //    Reverse the order of the arguments
4198 58
                    krsort($args);
4199
4200 58
                    if (($passByReference) && ($argCount == 0)) {
4201 1
                        $args[] = $cellID;
4202 1
                        $argArrayVals[] = $this->showValue($cellID);
4203
                    }
4204
4205 58
                    if ($functionName != 'MKMATRIX') {
4206 58
                        if ($this->debugLog->getWriteDebugLog()) {
4207
                            krsort($argArrayVals);
4208
                            $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )');
4209
                        }
4210
                    }
4211
4212
                    //    Process the argument with the appropriate function call
4213 58
                    $args = $this->addCellReference($args, $passCellReference, $functionCall, $pCell);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $functionCall does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $passCellReference does not seem to be defined for all execution paths leading up to this point.
Loading history...
4214
4215 58
                    if (!is_array($functionCall)) {
4216 1
                        foreach ($args as &$arg) {
4217
                            $arg = Functions::flattenSingleValue($arg);
4218
                        }
4219 1
                        unset($arg);
4220
                    }
4221
4222 58
                    $result = call_user_func_array($functionCall, $args);
4223
4224 58
                    if ($functionName != 'MKMATRIX') {
4225 58
                        $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
4226
                    }
4227 58
                    $stack->push('Value', self::wrapResult($result));
4228 58
                    if (isset($storeKey)) {
4229 58
                        $branchStore[$storeKey] = $result;
4230
                    }
4231
                }
4232
            } else {
4233
                // if the token is a number, boolean, string or an Excel error, push it onto the stack
4234 147
                if (isset(self::$excelConstants[strtoupper($token)])) {
4235
                    $excelConstant = strtoupper($token);
4236
                    $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
4237
                    if (isset($storeKey)) {
4238
                        $branchStore[$storeKey] = self::$excelConstants[$excelConstant];
4239
                    }
4240
                    $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant]));
4241 147
                } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == '"') || ($token[0] == '#')) {
4242 147
                    $stack->push('Value', $token);
4243 147
                    if (isset($storeKey)) {
4244 147
                        $branchStore[$storeKey] = $token;
4245
                    }
4246
                    // if the token is a named range, push the named range name onto the stack
4247 5
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) {
4248 5
                    $namedRange = $matches[6];
4249 5
                    $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
4250
4251 5
                    $cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
4252 5
                    $pCell->attach($pCellParent);
4253 5
                    $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
4254 5
                    $stack->push('Named Range', $cellValue, $namedRange);
4255 5
                    if (isset($storeKey)) {
4256 5
                        $branchStore[$storeKey] = $cellValue;
4257
                    }
4258
                } else {
4259
                    return $this->raiseFormulaError("undefined variable '$token'");
4260
                }
4261
            }
4262
        }
4263
        // when we're out of tokens, the stack should have a single element, the final result
4264 162
        if ($stack->count() != 1) {
4265
            return $this->raiseFormulaError('internal error');
4266
        }
4267 162
        $output = $stack->pop();
4268 162
        $output = $output['value'];
4269
4270 162
        return $output;
4271
    }
4272
4273 35
    private function validateBinaryOperand(&$operand, &$stack)
4274
    {
4275 35
        if (is_array($operand)) {
4276 23
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
4277
                do {
4278 23
                    $operand = array_pop($operand);
4279 23
                } while (is_array($operand));
4280
            }
4281
        }
4282
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
4283 35
        if (is_string($operand)) {
4284
            //    We only need special validations for the operand if it is a string
4285
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
4286 2
            if ($operand > '' && $operand[0] == '"') {
4287
                $operand = self::unwrapResult($operand);
4288
            }
4289
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
4290 2
            if (!is_numeric($operand)) {
4291
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
4292 2
                if ($operand > '' && $operand[0] == '#') {
4293
                    $stack->push('Value', $operand);
4294
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand));
4295
4296
                    return false;
4297 2
                } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
4298
                    //    If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
4299 2
                    $stack->push('Value', '#VALUE!');
4300 2
                    $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!'));
4301
4302 2
                    return false;
4303
                }
4304
            }
4305
        }
4306
4307
        //    return a true if the value of the operand is one that we can use in normal binary operations
4308 34
        return true;
4309
    }
4310
4311
    /**
4312
     * @param null|string $cellID
4313
     * @param mixed $operand1
4314
     * @param mixed $operand2
4315
     * @param string $operation
4316
     * @param Stack $stack
4317
     * @param bool $recursingArrays
4318
     *
4319
     * @return mixed
4320
     */
4321 111
    private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
4322
    {
4323
        //    If we're dealing with matrix operations, we want a matrix result
4324 111
        if ((is_array($operand1)) || (is_array($operand2))) {
4325 18
            $result = [];
4326 18
            if ((is_array($operand1)) && (!is_array($operand2))) {
4327 16
                foreach ($operand1 as $x => $operandData) {
4328 16
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2));
4329 16
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack);
4330 16
                    $r = $stack->pop();
4331 16
                    $result[$x] = $r['value'];
4332
                }
4333 2
            } elseif ((!is_array($operand1)) && (is_array($operand2))) {
4334
                foreach ($operand2 as $x => $operandData) {
4335
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData));
4336
                    $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack);
4337
                    $r = $stack->pop();
4338
                    $result[$x] = $r['value'];
4339
                }
4340
            } else {
4341 2
                if (!$recursingArrays) {
4342 2
                    self::checkMatrixOperands($operand1, $operand2, 2);
4343
                }
4344 2
                foreach ($operand1 as $x => $operandData) {
4345 2
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x]));
4346 2
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true);
4347 2
                    $r = $stack->pop();
4348 2
                    $result[$x] = $r['value'];
4349
                }
4350
            }
4351
            //    Log the result details
4352 18
            $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result));
4353
            //    And push the result onto the stack
4354 18
            $stack->push('Array', $result);
4355
4356 18
            return $result;
4357
        }
4358
4359
        //    Simple validate the two operands if they are string values
4360 111
        if (is_string($operand1) && $operand1 > '' && $operand1[0] == '"') {
4361 56
            $operand1 = self::unwrapResult($operand1);
4362
        }
4363 111
        if (is_string($operand2) && $operand2 > '' && $operand2[0] == '"') {
4364 71
            $operand2 = self::unwrapResult($operand2);
4365
        }
4366
4367
        // Use case insensitive comparaison if not OpenOffice mode
4368 111
        if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
4369 111
            if (is_string($operand1)) {
4370 63
                $operand1 = strtoupper($operand1);
4371
            }
4372 111
            if (is_string($operand2)) {
4373 73
                $operand2 = strtoupper($operand2);
4374
            }
4375
        }
4376
4377 111
        $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE;
4378
4379
        //    execute the necessary operation
4380 111
        switch ($operation) {
4381
            //    Greater than
4382 111
            case '>':
4383 21
                if ($useLowercaseFirstComparison) {
4384 9
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0;
4385
                } else {
4386 21
                    $result = ($operand1 > $operand2);
4387
                }
4388
4389 21
                break;
4390
            //    Less than
4391 94
            case '<':
4392 13
                if ($useLowercaseFirstComparison) {
4393 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0;
4394
                } else {
4395 13
                    $result = ($operand1 < $operand2);
4396
                }
4397
4398 13
                break;
4399
            //    Equality
4400 82
            case '=':
4401 43
                if (is_numeric($operand1) && is_numeric($operand2)) {
4402 11
                    $result = (abs($operand1 - $operand2) < $this->delta);
4403
                } else {
4404 36
                    $result = strcmp($operand1, $operand2) == 0;
4405
                }
4406
4407 43
                break;
4408
            //    Greater than or equal
4409 39
            case '>=':
4410 9
                if (is_numeric($operand1) && is_numeric($operand2)) {
4411 5
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2));
4412 4
                } elseif ($useLowercaseFirstComparison) {
4413 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0;
4414
                } else {
4415 4
                    $result = strcmp($operand1, $operand2) >= 0;
4416
                }
4417
4418 9
                break;
4419
            //    Less than or equal
4420 30
            case '<=':
4421 11
                if (is_numeric($operand1) && is_numeric($operand2)) {
4422 7
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2));
4423 4
                } elseif ($useLowercaseFirstComparison) {
4424 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0;
4425
                } else {
4426 4
                    $result = strcmp($operand1, $operand2) <= 0;
4427
                }
4428
4429 11
                break;
4430
            //    Inequality
4431 19
            case '<>':
4432 19
                if (is_numeric($operand1) && is_numeric($operand2)) {
4433 3
                    $result = (abs($operand1 - $operand2) > 1E-14);
4434
                } else {
4435 16
                    $result = strcmp($operand1, $operand2) != 0;
4436
                }
4437
4438 19
                break;
4439
        }
4440
4441
        //    Log the result details
4442 111
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
4443
        //    And push the result onto the stack
4444 111
        $stack->push('Value', $result);
4445
4446 111
        return $result;
4447
    }
4448
4449
    /**
4450
     * Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
4451
     *
4452
     * @param string $str1 First string value for the comparison
4453
     * @param string $str2 Second string value for the comparison
4454
     *
4455
     * @return int
4456
     */
4457 21
    private function strcmpLowercaseFirst($str1, $str2)
4458
    {
4459 21
        $inversedStr1 = Shared\StringHelper::strCaseReverse($str1);
4460 21
        $inversedStr2 = Shared\StringHelper::strCaseReverse($str2);
4461
4462 21
        return strcmp($inversedStr1, $inversedStr2);
4463
    }
4464
4465
    /**
4466
     * @param mixed $operand1
4467
     * @param mixed $operand2
4468
     * @param mixed $operation
4469
     * @param string $matrixFunction
4470
     * @param mixed $stack
4471
     *
4472
     * @return bool|mixed
4473
     */
4474 35
    private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack)
4475
    {
4476
        //    Validate the two operands
4477 35
        if (!$this->validateBinaryOperand($operand1, $stack)) {
4478 2
            return false;
4479
        }
4480 34
        if (!$this->validateBinaryOperand($operand2, $stack)) {
4481 1
            return false;
4482
        }
4483
4484
        //    If either of the operands is a matrix, we need to treat them both as matrices
4485
        //        (converting the other operand to a matrix if need be); then perform the required
4486
        //        matrix operation
4487 34
        if ((is_array($operand1)) || (is_array($operand2))) {
4488
            //    Ensure that both operands are arrays/matrices of the same size
4489
            self::checkMatrixOperands($operand1, $operand2, 2);
4490
4491
            try {
4492
                //    Convert operand 1 from a PHP array to a matrix
4493
                $matrix = new Shared\JAMA\Matrix($operand1);
4494
                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4495
                $matrixResult = $matrix->$matrixFunction($operand2);
4496
                $result = $matrixResult->getArray();
4497
            } catch (\Exception $ex) {
4498
                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4499
                $result = '#VALUE!';
4500
            }
4501
        } else {
4502 34
            if ((Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) &&
4503 34
                ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) ||
4504 34
                 (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0))) {
4505
                $result = Functions::VALUE();
4506
            } else {
4507
                //    If we're dealing with non-matrix operations, execute the necessary operation
4508 34
                switch ($operation) {
4509
                    //    Addition
4510 34
                    case '+':
4511 25
                        $result = $operand1 + $operand2;
4512
4513 25
                        break;
4514
                    //    Subtraction
4515 26
                    case '-':
4516 7
                        $result = $operand1 - $operand2;
4517
4518 7
                        break;
4519
                    //    Multiplication
4520 24
                    case '*':
4521 19
                        $result = $operand1 * $operand2;
4522
4523 19
                        break;
4524
                    //    Division
4525 8
                    case '/':
4526 8
                        if ($operand2 == 0) {
4527
                            //    Trap for Divide by Zero error
4528 8
                            $stack->push('Value', '#DIV/0!');
4529 8
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails('#DIV/0!'));
4530
4531 8
                            return false;
4532
                        }
4533
                            $result = $operand1 / $operand2;
4534
4535
                        break;
4536
                    //    Power
4537
                    case '^':
4538
                        $result = pow($operand1, $operand2);
4539
4540
                        break;
4541
                }
4542
            }
4543
        }
4544
4545
        //    Log the result details
4546 29
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
4547
        //    And push the result onto the stack
4548 29
        $stack->push('Value', $result);
4549
4550 29
        return $result;
4551
    }
4552
4553
    // trigger an error, but nicely, if need be
4554
    protected function raiseFormulaError($errorMessage)
4555
    {
4556
        $this->formulaError = $errorMessage;
4557
        $this->cyclicReferenceStack->clear();
4558
        if (!$this->suppressFormulaErrors) {
4559
            throw new Exception($errorMessage);
4560
        }
4561
        trigger_error($errorMessage, E_USER_ERROR);
4562
4563
        return false;
4564
    }
4565
4566
    /**
4567
     * Extract range values.
4568
     *
4569
     * @param string &$pRange String based range representation
4570
     * @param Worksheet $pSheet Worksheet
4571
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4572
     *
4573
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4574
     */
4575 72
    public function extractCellRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4576
    {
4577
        // Return value
4578 72
        $returnValue = [];
4579
4580 72
        if ($pSheet !== null) {
4581 72
            $pSheetName = $pSheet->getTitle();
4582 72
            if (strpos($pRange, '!') !== false) {
4583
                [$pSheetName, $pRange] = Worksheet::extractSheetTitle($pRange, true);
4584
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4585
            }
4586
4587
            // Extract range
4588 72
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4589 72
            $pRange = $pSheetName . '!' . $pRange;
4590 72
            if (!isset($aReferences[1])) {
4591 72
                $currentCol = '';
4592 72
                $currentRow = 0;
4593
                //    Single cell in range
4594 72
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
4595 72
                if ($pSheet->cellExists($aReferences[0])) {
4596 72
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4597
                } else {
4598 72
                    $returnValue[$currentRow][$currentCol] = null;
4599
                }
4600
            } else {
4601
                // Extract cell data for all cells in the range
4602 55
                foreach ($aReferences as $reference) {
4603 55
                    $currentCol = '';
4604 55
                    $currentRow = 0;
4605
                    // Extract range
4606 55
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
4607 55
                    if ($pSheet->cellExists($reference)) {
4608 55
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4609
                    } else {
4610 12
                        $returnValue[$currentRow][$currentCol] = null;
4611
                    }
4612
                }
4613
            }
4614
        }
4615
4616 72
        return $returnValue;
4617
    }
4618
4619
    /**
4620
     * Extract range values.
4621
     *
4622
     * @param string &$pRange String based range representation
4623
     * @param Worksheet $pSheet Worksheet
4624
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4625
     *
4626
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4627
     */
4628 5
    public function extractNamedRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4629
    {
4630
        // Return value
4631 5
        $returnValue = [];
4632
4633 5
        if ($pSheet !== null) {
4634 5
            $pSheetName = $pSheet->getTitle();
0 ignored issues
show
Unused Code introduced by
The assignment to $pSheetName is dead and can be removed.
Loading history...
4635 5
            if (strpos($pRange, '!') !== false) {
4636
                [$pSheetName, $pRange] = Worksheet::extractSheetTitle($pRange, true);
4637
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4638
            }
4639
4640
            // Named range?
4641 5
            $namedRange = NamedRange::resolveRange($pRange, $pSheet);
1 ignored issue
show
Bug introduced by
It seems like $pSheet can also be of type null; however, parameter $pSheet of PhpOffice\PhpSpreadsheet...edRange::resolveRange() does only seem to accept PhpOffice\PhpSpreadsheet\Worksheet\Worksheet, 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

4641
            $namedRange = NamedRange::resolveRange($pRange, /** @scrutinizer ignore-type */ $pSheet);
Loading history...
4642 5
            if ($namedRange !== null) {
4643 2
                $pSheet = $namedRange->getWorksheet();
4644 2
                $pRange = $namedRange->getRange();
4645 2
                $splitRange = Coordinate::splitRange($pRange);
4646
                //    Convert row and column references
4647 2
                if (ctype_alpha($splitRange[0][0])) {
4648
                    $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
4649 2
                } elseif (ctype_digit($splitRange[0][0])) {
4650 2
                    $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
4651
                }
4652
            } else {
4653 3
                return Functions::REF();
4654
            }
4655
4656
            // Extract range
4657 2
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4658 2
            if (!isset($aReferences[1])) {
4659
                //    Single cell (or single column or row) in range
4660 1
                [$currentCol, $currentRow] = Coordinate::coordinateFromString($aReferences[0]);
4661 1
                if ($pSheet->cellExists($aReferences[0])) {
4662 1
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4663
                } else {
4664 1
                    $returnValue[$currentRow][$currentCol] = null;
4665
                }
4666
            } else {
4667
                // Extract cell data for all cells in the range
4668 1
                foreach ($aReferences as $reference) {
4669
                    // Extract range
4670 1
                    [$currentCol, $currentRow] = Coordinate::coordinateFromString($reference);
4671 1
                    if ($pSheet->cellExists($reference)) {
4672 1
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4673
                    } else {
4674
                        $returnValue[$currentRow][$currentCol] = null;
4675
                    }
4676
                }
4677
            }
4678
        }
4679
4680 2
        return $returnValue;
4681
    }
4682
4683
    /**
4684
     * Is a specific function implemented?
4685
     *
4686
     * @param string $pFunction Function Name
4687
     *
4688
     * @return bool
4689
     */
4690 3
    public function isImplemented($pFunction)
4691
    {
4692 3
        $pFunction = strtoupper($pFunction);
4693 3
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
4694
4695 3
        return !$notImplemented;
4696
    }
4697
4698
    /**
4699
     * Get a list of all implemented functions as an array of function objects.
4700
     *
4701
     * @return array of Category
4702
     */
4703
    public function getFunctions()
4704
    {
4705
        return self::$phpSpreadsheetFunctions;
4706
    }
4707
4708
    /**
4709
     * Get a list of implemented Excel function names.
4710
     *
4711
     * @return array
4712
     */
4713 2
    public function getImplementedFunctionNames()
4714
    {
4715 2
        $returnValue = [];
4716 2
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
4717 2
            if ($this->isImplemented($functionName)) {
4718 2
                $returnValue[] = $functionName;
4719
            }
4720
        }
4721
4722 2
        return $returnValue;
4723
    }
4724
4725
    /**
4726
     * Add cell reference if needed while making sure that it is the last argument.
4727
     *
4728
     * @param array $args
4729
     * @param bool $passCellReference
4730
     * @param array|string $functionCall
4731
     * @param null|Cell $pCell
4732
     *
4733
     * @return array
4734
     */
4735 58
    private function addCellReference(array $args, $passCellReference, $functionCall, Cell $pCell = null)
4736
    {
4737 58
        if ($passCellReference) {
4738 4
            if (is_array($functionCall)) {
4739 4
                $className = $functionCall[0];
4740 4
                $methodName = $functionCall[1];
4741
4742 4
                $reflectionMethod = new \ReflectionMethod($className, $methodName);
4743 4
                $argumentCount = count($reflectionMethod->getParameters());
4744 4
                while (count($args) < $argumentCount - 1) {
4745 1
                    $args[] = null;
4746
                }
4747
            }
4748
4749 4
            $args[] = $pCell;
4750
        }
4751
4752 58
        return $args;
4753
    }
4754
4755 25
    private function getUnusedBranchStoreKey()
4756
    {
4757 25
        $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter;
4758 25
        ++$this->branchStoreKeyCounter;
4759
4760 25
        return $storeKeyValue;
4761
    }
4762
4763
    private function getTokensAsString($tokens)
4764
    {
4765
        $tokensStr = array_map(function ($token) {
4766
            $value = $token['value'] ?? 'no value';
4767
            while (is_array($value)) {
4768
                $value = array_pop($value);
4769
            }
4770
4771
            return $value;
4772
        }, $tokens);
4773
        $str = '[ ' . implode(' | ', $tokensStr) . ' ]';
4774
4775
        return $str;
4776
    }
4777
}
4778