Passed
Push — master ( 83a505...14d807 )
by Adrien
09:50 queued 14s
created

Calculation::setLocale()   D

Complexity

Conditions 19
Paths 140

Size

Total Lines 78
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 19.4348

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 42
cts 47
cp 0.8936
crap 19.4348
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,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
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 285
    public function __construct(Spreadsheet $spreadsheet = null)
2218
    {
2219 285
        $this->delta = 1 * pow(10, 0 - ini_get('precision'));
2220
2221 285
        $this->spreadsheet = $spreadsheet;
2222 285
        $this->cyclicReferenceStack = new CyclicReferenceStack();
2223 285
        $this->debugLog = new Logger($this->cyclicReferenceStack);
2224 285
    }
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 695
    public static function getInstance(Spreadsheet $spreadsheet = null)
2246
    {
2247 695
        if ($spreadsheet !== null) {
2248 163
            $instance = $spreadsheet->getCalculationEngine();
2249 163
            if (isset($instance)) {
2250 163
                return $instance;
2251
            }
2252
        }
2253
2254 561
        if (!isset(self::$instance) || (self::$instance === null)) {
2255 14
            self::$instance = new self();
2256
        }
2257
2258 561
        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 5
    public function flushInstance()
2266
    {
2267 5
        $this->clearCalculationCache();
2268 5
        $this->clearBranchStore();
2269 5
    }
2270
2271
    /**
2272
     * Get the Logger for this calculation engine instance.
2273
     *
2274
     * @return Logger
2275
     */
2276 121
    public function getDebugLog()
2277
    {
2278 121
        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 15
            ($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 5
    public function clearCalculationCache()
2382
    {
2383 5
        $this->calculationCache = [];
2384 5
    }
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 285
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName)
2405
    {
2406 285
        if (isset($this->calculationCache[$fromWorksheetName])) {
2407
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2408
            unset($this->calculationCache[$fromWorksheetName]);
2409
        }
2410 285
    }
2411
2412
    /**
2413
     * Enable/disable calculation cache.
2414
     *
2415
     * @param bool $pValue
2416
     * @param mixed $enabled
2417
     */
2418 10
    public function setBranchPruningEnabled($enabled)
2419
    {
2420 10
        $this->branchPruningEnabled = $enabled;
2421 10
    }
2422
2423
    public function enableBranchPruning()
2424
    {
2425
        $this->setBranchPruningEnabled(true);
2426
    }
2427
2428 10
    public function disableBranchPruning()
2429
    {
2430 10
        $this->setBranchPruningEnabled(false);
2431 10
    }
2432
2433 5
    public function clearBranchStore()
2434
    {
2435 5
        $this->branchStoreKeyCounter = 0;
2436 5
    }
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 488
    public function setLocale($locale)
2456
    {
2457
        //    Identify our locale and language
2458 488
        $language = $locale = strtolower($locale);
2459 488
        if (strpos($locale, '_') !== false) {
2460 488
            [$language] = explode('_', $locale);
2461
        }
2462 488
        if (count(self::$validLocaleLanguages) == 1) {
2463 1
            self::loadLocales();
2464
        }
2465
        //    Test whether we have any language data for this language (any locale)
2466 488
        if (in_array($language, self::$validLocaleLanguages)) {
2467
            //    initialise language/locale settings
2468 488
            self::$localeFunctions = [];
2469 488
            self::$localeArgumentSeparator = ',';
2470 488
            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 488
            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 488
            self::$functionReplaceFromExcel = self::$functionReplaceToExcel =
2526 488
            self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
2527 488
            self::$localeLanguage = $locale;
2528
2529 488
            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 6
    public static function translateSeparator($fromSeparator, $toSeparator, $formula, &$inBraces)
2544
    {
2545 6
        $strlen = mb_strlen($formula);
2546 6
        for ($i = 0; $i < $strlen; ++$i) {
2547 6
            $chr = mb_substr($formula, $i, 1);
2548
            switch ($chr) {
2549 6
                case '{':
2550
                    $inBraces = true;
2551
2552
                    break;
2553 6
                case '}':
2554
                    $inBraces = false;
2555
2556
                    break;
2557 6
                case $fromSeparator:
2558
                    if (!$inBraces) {
2559
                        $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
2560
                    }
2561
            }
2562
        }
2563
2564 6
        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 66
    public static function localeFunc($function)
2666
    {
2667 66
        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 66
        return $function;
2679
    }
2680
2681
    /**
2682
     * Wrap string values in quotes.
2683
     *
2684
     * @param mixed $value
2685
     *
2686
     * @return mixed
2687
     */
2688 126
    public static function wrapResult($value)
2689
    {
2690 126
        if (is_string($value)) {
2691
            //    Error values cannot be "wrapped"
2692 107
            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 98
            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 133
    public static function unwrapResult($value)
2714
    {
2715 133
        if (is_string($value)) {
2716 107
            if ((isset($value[0])) && ($value[0] == '"') && (substr($value, -1) == '"')) {
2717 107
                return substr($value, 1, -1);
2718
            }
2719
            //    Convert numeric errors to NAN error
2720 75
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2721
            return Functions::NAN();
2722
        }
2723
2724 77
        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 66
    public function calculateCellValue(Cell $pCell = null, $resetLog = true)
2757
    {
2758 66
        if ($pCell === null) {
2759
            return null;
2760
        }
2761
2762 66
        $returnArrayAsType = self::$returnArrayAsType;
2763 66
        if ($resetLog) {
2764
            //    Initialise the logging settings if requested
2765 65
            $this->formulaError = null;
2766 65
            $this->debugLog->clearLog();
2767 65
            $this->cyclicReferenceStack->clear();
2768 65
            $this->cyclicFormulaCounter = 1;
2769
2770 65
            self::$returnArrayAsType = self::RETURN_ARRAY_AS_ARRAY;
2771
        }
2772
2773
        //    Execute the calculation for the cell formula
2774 66
        $this->cellStack[] = [
2775 66
            'sheet' => $pCell->getWorksheet()->getTitle(),
2776 66
            'cell' => $pCell->getCoordinate(),
2777
        ];
2778
2779
        try {
2780 66
            $result = self::unwrapResult($this->_calculateFormulaValue($pCell->getValue(), $pCell->getCoordinate(), $pCell));
2781 66
            $cellAddress = array_pop($this->cellStack);
2782 66
            $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 66
        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 66
        self::$returnArrayAsType = $returnArrayAsType;
2815
2816 66
        if ($result === null && $pCell->getWorksheet()->getSheetView()->getShowZeros()) {
2817
            return 0;
2818 66
        } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
2819
            return Functions::NAN();
2820
        }
2821
2822 66
        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 6
    public function parseFormula($formula)
2833
    {
2834
        //    Basic validation that this is indeed a formula
2835
        //    We return an empty array if not
2836 6
        $formula = trim($formula);
2837 6
        if ((!isset($formula[0])) || ($formula[0] != '=')) {
2838
            return [];
2839
        }
2840 6
        $formula = ltrim(substr($formula, 1));
2841 6
        if (!isset($formula[0])) {
2842
            return [];
2843
        }
2844
2845
        //    Parse the formula and return the token stack
2846 6
        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
        $resetCache = $this->getCalculationCacheEnabled();
2868 1
        if ($this->spreadsheet !== null && $cellID === null && $pCell === null) {
2869
            $cellID = 'A1';
2870
            $pCell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
2871
        } else {
2872
            //    Disable calculation cacheing because it only applies to cell calculations, not straight formulae
2873
            //    But don't actually flush any cache
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;
2887
        }
2888
2889 1
        return $result;
2890
    }
2891
2892
    /**
2893
     * @param string $cellReference
2894
     * @param mixed $cellValue
2895
     *
2896
     * @return bool
2897
     */
2898 65
    public function getValueFromCache($cellReference, &$cellValue)
2899
    {
2900
        // Is calculation cacheing enabled?
2901
        // Is the value present in calculation cache?
2902 65
        $this->debugLog->writeDebugLog('Testing cache value for cell ', $cellReference);
2903 65
        if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
2904 52
            $this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache');
2905
            // Return the cached result
2906
2907 52
            $cellValue = $this->calculationCache[$cellReference];
2908
2909 52
            return true;
2910
        }
2911
2912 65
        return false;
2913
    }
2914
2915
    /**
2916
     * @param string $cellReference
2917
     * @param mixed $cellValue
2918
     */
2919 65
    public function saveValueToCache($cellReference, $cellValue)
2920
    {
2921 65
        if ($this->calculationCacheEnabled) {
2922 64
            $this->calculationCache[$cellReference] = $cellValue;
2923
        }
2924 65
    }
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 167
    public function _calculateFormulaValue($formula, $cellID = null, Cell $pCell = null)
2938
    {
2939 167
        $cellValue = null;
2940
2941
        //  Quote-Prefixed cell values cannot be formulae, but are treated as strings
2942 167
        if ($pCell !== null && $pCell->getStyle()->getQuotePrefix() === true) {
2943 1
            return self::wrapResult((string) $formula);
2944
        }
2945
2946 166
        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 165
        $formula = trim($formula);
2953 165
        if ($formula[0] != '=') {
2954
            return self::wrapResult($formula);
2955
        }
2956 165
        $formula = ltrim(substr($formula, 1));
2957 165
        if (!isset($formula[0])) {
2958
            return self::wrapResult($formula);
2959
        }
2960
2961 165
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
2962 165
        $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
2963 165
        $wsCellReference = $wsTitle . '!' . $cellID;
2964
2965 165
        if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
2966 52
            return $cellValue;
2967
        }
2968
2969 165
        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 165
        $this->cyclicReferenceStack->push($wsCellReference);
2991 165
        $cellValue = $this->processTokenStack($this->_parseFormula($formula, $pCell), $cellID, $pCell);
2992 165
        $this->cyclicReferenceStack->pop();
2993
2994
        // Save to calculation cache
2995 165
        if ($cellID !== null) {
2996 65
            $this->saveValueToCache($wsCellReference, $cellValue);
2997
        }
2998
2999
        //    Return the calculated value
3000 165
        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 162
    private function showValue($value)
3171
    {
3172 162
        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 162
        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 163
    private function showTypeDetails($value)
3209
    {
3210 163
        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 163
    }
3238
3239
    /**
3240
     * @param string $formula
3241
     *
3242
     * @return false|string False indicates an error
3243
     */
3244 171
    private function convertMatrixReferences($formula)
3245
    {
3246 171
        static $matrixReplaceFrom = ['{', ';', '}'];
3247 171
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
3248
3249
        //    Convert any Excel matrix references to the MKMATRIX() function
3250 171
        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 '}'");
3280
                }
3281
3282
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
3283
            } elseif ($openCount > $closeCount) {
3284
                if ($closeCount > 0) {
3285
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
3286
                }
3287
3288
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
3289
            }
3290
        }
3291
3292 171
        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 171
    private function _parseFormula($formula, Cell $pCell = null)
3340
    {
3341 171
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
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 171
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3348
3349 171
        $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION .
3350 171
                                '|' . self::CALCULATION_REGEXP_CELLREF .
3351 171
                                '|' . self::CALCULATION_REGEXP_NUMBER .
3352 171
                                '|' . self::CALCULATION_REGEXP_STRING .
3353 171
                                '|' . self::CALCULATION_REGEXP_OPENBRACE .
3354 171
                                '|' . self::CALCULATION_REGEXP_NAMEDRANGE .
3355 171
                                '|' . self::CALCULATION_REGEXP_ERROR .
3356 171
                                ')/si';
3357
3358
        //    Start with initialisation
3359 171
        $index = 0;
3360 171
        $stack = new Stack();
3361 171
        $output = [];
3362 171
        $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 171
        $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 171
        $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 171
        $pendingStoreKeysStack = [];
3372 171
        $expectingConditionMap = []; // ['storeKey' => true, ...]
3373 171
        $expectingThenMap = []; // ['storeKey' => true, ...]
3374 171
        $expectingElseMap = []; // ['storeKey' => true, ...]
3375 171
        $parenthesisDepthMap = []; // ['storeKey' => 4, ...]
3376
3377
        //    The guts of the lexical parser
3378
        //    Loop through the formula extracting each operator and operand in turn
3379 171
        while (true) {
3380
            // Branch pruning: we adapt the output item to the context (it will
3381
            // be used to limit its computation)
3382 171
            $currentCondition = null;
3383 171
            $currentOnlyIf = null;
3384 171
            $currentOnlyIfNot = null;
3385 171
            $previousStoreKey = null;
3386 171
            $pendingStoreKey = end($pendingStoreKeysStack);
3387
3388 171
            if ($this->branchPruningEnabled) {
3389
                // this is a condition ?
3390 171
                if (isset($expectingConditionMap[$pendingStoreKey]) && $expectingConditionMap[$pendingStoreKey]) {
3391 27
                    $currentCondition = $pendingStoreKey;
3392 27
                    $stackDepth = count($pendingStoreKeysStack);
3393 27
                    if ($stackDepth > 1) { // nested if
3394 7
                        $previousStoreKey = $pendingStoreKeysStack[$stackDepth - 2];
3395
                    }
3396
                }
3397 171
                if (isset($expectingThenMap[$pendingStoreKey]) && $expectingThenMap[$pendingStoreKey]) {
3398 27
                    $currentOnlyIf = $pendingStoreKey;
3399 171
                } elseif (isset($previousStoreKey)) {
3400 7
                    if (isset($expectingThenMap[$previousStoreKey]) && $expectingThenMap[$previousStoreKey]) {
3401 2
                        $currentOnlyIf = $previousStoreKey;
3402
                    }
3403
                }
3404 171
                if (isset($expectingElseMap[$pendingStoreKey]) && $expectingElseMap[$pendingStoreKey]) {
3405 24
                    $currentOnlyIfNot = $pendingStoreKey;
3406 171
                } elseif (isset($previousStoreKey)) {
3407 7
                    if (isset($expectingElseMap[$previousStoreKey]) && $expectingElseMap[$previousStoreKey]) {
3408 5
                        $currentOnlyIfNot = $previousStoreKey;
3409
                    }
3410
                }
3411
            }
3412
3413 171
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
3414 171
            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 171
            $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
3420
3421 171
            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 171
            } elseif ($opCharacter == '%' && $expectingOperator) {
3426
                //    Put a percentage on the stack
3427
                $stack->push('Unary Operator', '%', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3428
                ++$index;
3429 171
            } elseif ($opCharacter == '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
3430
                ++$index; //    Drop the redundant plus symbol
3431 171
            } 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 171
            } elseif ((isset(self::$operators[$opCharacter]) or $isOperandOrFunction) && $expectingOperator) {    //    Are we putting an operator on the stack?
3434 160
                while ($stack->count() > 0 &&
3435 160
                    ($o2 = $stack->last()) &&
3436 160
                    isset(self::$operators[$o2['value']]) &&
3437 160
                    @(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 160
                $stack->push('Binary Operator', $opCharacter, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3443
3444 160
                ++$index;
3445 160
                $expectingOperator = false;
3446 171
            } elseif ($opCharacter == ')' && $expectingOperator) {            //    Are we expecting to close a parenthesis?
3447 66
                $expectingOperand = false;
3448 66
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3449 50
                    if ($o2 === null) {
3450
                        return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"');
3451
                    }
3452 50
                    $output[] = $o2;
3453
                }
3454 66
                $d = $stack->last(2);
3455
3456
                // Branch pruning we decrease the depth whether is it a function
3457
                // call or a parenthesis
3458 66
                if (!empty($pendingStoreKey)) {
3459 27
                    $parenthesisDepthMap[$pendingStoreKey] -= 1;
3460
                }
3461
3462 66
                if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {    //    Did this parenthesis just close a function?
3463 66
                    if (!empty($pendingStoreKey) && $parenthesisDepthMap[$pendingStoreKey] == -1) {
3464
                        // we are closing an IF(
3465 27
                        if ($d['value'] != 'IF(') {
3466
                            return $this->raiseFormulaError('Parser bug we should be in an "IF("');
3467
                        }
3468 27
                        if ($expectingConditionMap[$pendingStoreKey]) {
3469
                            return $this->raiseFormulaError('We should not be expecting a condition');
3470
                        }
3471 27
                        $expectingThenMap[$pendingStoreKey] = false;
3472 27
                        $expectingElseMap[$pendingStoreKey] = false;
3473 27
                        $parenthesisDepthMap[$pendingStoreKey] -= 1;
3474 27
                        array_pop($pendingStoreKeysStack);
3475 27
                        unset($pendingStoreKey);
3476
                    }
3477
3478 66
                    $functionName = $matches[1]; //    Get the function name
3479 66
                    $d = $stack->pop();
3480 66
                    $argumentCount = $d['value']; //    See how many arguments there were (argument count is the next value stored on the stack)
3481 66
                    $output[] = $d; //    Dump the argument count on the output
3482 66
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
3483 66
                    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 66
                    } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3487 66
                        $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
3488 66
                        $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 66
                    $argumentCountError = false;
3494 66
                    if (is_numeric($expectedArgumentCount)) {
3495 32
                        if ($expectedArgumentCount < 0) {
3496 1
                            if ($argumentCount > abs($expectedArgumentCount)) {
3497
                                $argumentCountError = true;
3498 1
                                $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
3499
                            }
3500
                        } else {
3501 31
                            if ($argumentCount != $expectedArgumentCount) {
3502
                                $argumentCountError = true;
3503 32
                                $expectedArgumentCountString = $expectedArgumentCount;
3504
                            }
3505
                        }
3506 58
                    } elseif ($expectedArgumentCount != '*') {
3507 58
                        $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 58
                        switch ($argMatch[2]) {
3509 58
                            case '+':
3510 47
                                if ($argumentCount < $argMatch[1]) {
3511
                                    $argumentCountError = true;
3512
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
3513
                                }
3514
3515 47
                                break;
3516 32
                            case '-':
3517 28
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
3518
                                    $argumentCountError = true;
3519
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
3520
                                }
3521
3522 28
                                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 66
                    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 66
                ++$index;
3537 171
            } elseif ($opCharacter == ',') {            //    Is this the separator for function arguments?
3538 47
                if (!empty($pendingStoreKey) &&
3539 47
                    $parenthesisDepthMap[$pendingStoreKey] == 0
3540
                ) {
3541
                    // We must go to the IF next argument
3542 27
                    if ($expectingConditionMap[$pendingStoreKey]) {
3543 27
                        $expectingConditionMap[$pendingStoreKey] = false;
3544 27
                        $expectingThenMap[$pendingStoreKey] = true;
3545 24
                    } elseif ($expectingThenMap[$pendingStoreKey]) {
3546 24
                        $expectingThenMap[$pendingStoreKey] = false;
3547 24
                        $expectingElseMap[$pendingStoreKey] = true;
3548
                    } elseif ($expectingElseMap[$pendingStoreKey]) {
3549
                        return $this->raiseFormulaError('Reaching fourth argument of an IF');
3550
                    }
3551
                }
3552 47
                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 47
                if (($expectingOperand) || (!$expectingOperator)) {
3561
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3562
                }
3563
                // make sure there was a function
3564 47
                $d = $stack->last(2);
3565 47
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {
3566
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
3567
                }
3568 47
                $d = $stack->pop();
3569 47
                $itemStoreKey = $d['storeKey'] ?? null;
3570 47
                $itemOnlyIf = $d['onlyIf'] ?? null;
3571 47
                $itemOnlyIfNot = $d['onlyIfNot'] ?? null;
3572 47
                $stack->push($d['type'], ++$d['value'], $d['reference'], $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // increment the argument count
3573 47
                $stack->push('Brace', '(', null, $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // put the ( back on, we'll need to pop back to it again
3574 47
                $expectingOperator = false;
3575 47
                $expectingOperand = true;
3576 47
                ++$index;
3577 171
            } 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 171
            } elseif ($isOperandOrFunction && !$expectingOperator) {    // do we now have a function/variable/number?
3584 171
                $expectingOperator = true;
3585 171
                $expectingOperand = false;
3586 171
                $val = $match[1];
3587 171
                $length = strlen($val);
3588
3589 171
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
3590 66
                    $val = preg_replace('/\s/u', '', $val);
3591 66
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
3592 66
                        $valToUpper = strtoupper($val);
3593
                        // here $matches[1] will contain values like "IF"
3594
                        // and $val "IF("
3595 66
                        if ($this->branchPruningEnabled && ($valToUpper == 'IF(')) { // we handle a new if
3596 27
                            $pendingStoreKey = $this->getUnusedBranchStoreKey();
3597 27
                            $pendingStoreKeysStack[] = $pendingStoreKey;
3598 27
                            $expectingConditionMap[$pendingStoreKey] = true;
3599 27
                            $parenthesisDepthMap[$pendingStoreKey] = 0;
3600
                        } else { // this is not a if but we good deeper
3601 58
                            if (!empty($pendingStoreKey) && array_key_exists($pendingStoreKey, $parenthesisDepthMap)) {
3602 8
                                $parenthesisDepthMap[$pendingStoreKey] += 1;
3603
                            }
3604
                        }
3605
3606 66
                        $stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3607
                        // tests if the function is closed right after opening
3608 66
                        $ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
3609 66
                        if ($ax) {
3610 13
                            $stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3611 13
                            $expectingOperator = true;
3612
                        } else {
3613 65
                            $stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3614 65
                            $expectingOperator = false;
3615
                        }
3616 66
                        $stack->push('Brace', '(');
3617
                    } else {    // it's a var w/ implicit multiplication
3618 66
                        $output[] = ['type' => 'Value', 'value' => $matches[1], 'reference' => null];
3619
                    }
3620 170
                } 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 78
                    $testPrevOp = $stack->last(1);
3626 78
                    if ($testPrevOp !== null && $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 78
                    $outputItem = $stack->getStackItem('Cell Reference', $val, $val, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3642
3643 78
                    $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 143
                    $testPrevOp = $stack->last(1);
3647 143
                    if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
3648 1
                        $startRowColRef = $output[count($output) - 1]['value'];
3649 1
                        [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
3650 1
                        $rangeSheetRef = $rangeWS1;
3651 1
                        if ($rangeWS1 != '') {
3652
                            $rangeWS1 .= '!';
3653
                        }
3654 1
                        [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
3655 1
                        if ($rangeWS2 != '') {
3656
                            $rangeWS2 .= '!';
3657
                        } else {
3658 1
                            $rangeWS2 = $rangeWS1;
3659
                        }
3660 1
                        $refSheet = $pCellParent;
3661 1
                        if ($pCellParent !== null && $rangeSheetRef !== $pCellParent->getTitle()) {
3662 1
                            $refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef);
3663
                        }
3664 1
                        if ((is_int($startRowColRef)) && (ctype_digit($val)) &&
3665 1
                            ($startRowColRef <= 1048576) && ($val <= 1048576)) {
3666
                            //    Row range
3667
                            $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestColumn() : 'XFD'; //    Max 16,384 columns for Excel2007
3668
                            $output[count($output) - 1]['value'] = $rangeWS1 . 'A' . $startRowColRef;
3669
                            $val = $rangeWS2 . $endRowColRef . $val;
3670 1
                        } elseif ((ctype_alpha($startRowColRef)) && (ctype_alpha($val)) &&
3671 1
                            (strlen($startRowColRef) <= 3) && (strlen($val) <= 3)) {
3672
                            //    Column range
3673
                            $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestRow() : 1048576; //    Max 1,048,576 rows for Excel2007
3674
                            $output[count($output) - 1]['value'] = $rangeWS1 . strtoupper($startRowColRef) . '1';
3675
                            $val = $rangeWS2 . $val . $endRowColRef;
3676
                        }
3677
                    }
3678
3679 143
                    $localeConstant = false;
3680 143
                    if ($opCharacter == '"') {
3681
                        //    UnEscape any quotes within the string
3682 86
                        $val = self::wrapResult(str_replace('""', '"', self::unwrapResult($val)));
3683 94
                    } elseif (is_numeric($val)) {
3684 92
                        if ((strpos($val, '.') !== false) || (stripos($val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
3685 20
                            $val = (float) $val;
3686
                        } else {
3687 92
                            $val = (int) $val;
3688
                        }
3689 8
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
3690 1
                        $excelConstant = trim(strtoupper($val));
3691 1
                        $val = self::$excelConstants[$excelConstant];
3692 7
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
3693
                        $val = self::$excelConstants[$localeConstant];
3694
                    }
3695 143
                    $details = $stack->getStackItem('Value', $val, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3696 143
                    if ($localeConstant) {
3697
                        $details['localeValue'] = $localeConstant;
3698
                    }
3699 143
                    $output[] = $details;
3700
                }
3701 171
                $index += $length;
3702
            } elseif ($opCharacter == '$') {    // absolute row or column range
3703
                ++$index;
3704
            } elseif ($opCharacter == ')') {    // miscellaneous error checking
3705
                if ($expectingOperand) {
3706
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3707
                    $expectingOperand = false;
3708
                    $expectingOperator = true;
3709
                } else {
3710
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
3711
                }
3712
            } elseif (isset(self::$operators[$opCharacter]) && !$expectingOperator) {
3713
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
3714
            } else {    // I don't even want to know what you did to get here
3715
                return $this->raiseFormulaError('Formula Error: An unexpected error occured');
3716
            }
3717
            //    Test for end of formula string
3718 171
            if ($index == strlen($formula)) {
3719
                //    Did we end with an operator?.
3720
                //    Only valid for the % unary operator
3721 171
                if ((isset(self::$operators[$opCharacter])) && ($opCharacter != '%')) {
3722
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
3723
                }
3724
3725 171
                break;
3726
            }
3727
            //    Ignore white space
3728 169
            while (($formula[$index] == "\n") || ($formula[$index] == "\r")) {
3729
                ++$index;
3730
            }
3731 169
            if ($formula[$index] == ' ') {
3732 64
                while ($formula[$index] == ' ') {
3733 64
                    ++$index;
3734
                }
3735
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
3736
                //        Cell References) then we have an INTERSECTION operator
3737 64
                if (($expectingOperator) && (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
3738 64
                    ($output[count($output) - 1]['type'] == 'Cell Reference')) {
3739
                    while ($stack->count() > 0 &&
3740
                        ($o2 = $stack->last()) &&
3741
                        isset(self::$operators[$o2['value']]) &&
3742
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3743
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3744
                    }
3745
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
3746
                    $expectingOperator = false;
3747
                }
3748
            }
3749
        }
3750
3751 171
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
3752 133
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
3753
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
3754
            }
3755 133
            $output[] = $op;
3756
        }
3757
3758 171
        return $output;
3759
    }
3760
3761 154
    private static function dataTestReference(&$operandData)
3762
    {
3763 154
        $operand = $operandData['value'];
3764 154
        if (($operandData['reference'] === null) && (is_array($operand))) {
3765 1
            $rKeys = array_keys($operand);
3766 1
            $rowKey = array_shift($rKeys);
3767 1
            $cKeys = array_keys(array_keys($operand[$rowKey]));
3768 1
            $colKey = array_shift($cKeys);
3769 1
            if (ctype_upper($colKey)) {
3770
                $operandData['reference'] = $colKey . $rowKey;
3771
            }
3772
        }
3773
3774 154
        return $operand;
3775
    }
3776
3777
    // evaluate postfix notation
3778
3779
    /**
3780
     * @param mixed $tokens
3781
     * @param null|string $cellID
3782
     * @param null|Cell $pCell
3783
     *
3784
     * @return bool
3785
     */
3786 165
    private function processTokenStack($tokens, $cellID = null, Cell $pCell = null)
3787
    {
3788 165
        if ($tokens == false) {
3789
            return false;
3790
        }
3791
3792
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
3793
        //        so we store the parent cell collection so that we can re-attach it when necessary
3794 165
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
3795 165
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
3796 165
        $stack = new Stack();
3797
3798
        // Stores branches that have been pruned
3799 165
        $fakedForBranchPruning = [];
3800
        // help us to know when pruning ['branchTestId' => true/false]
3801 165
        $branchStore = [];
3802
3803
        //    Loop through each token in turn
3804 165
        foreach ($tokens as $tokenData) {
3805 165
            $token = $tokenData['value'];
3806
3807
            // Branch pruning: skip useless resolutions
3808 165
            $storeKey = $tokenData['storeKey'] ?? null;
3809 165
            if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) {
3810 22
                $onlyIfStoreKey = $tokenData['onlyIf'];
3811 22
                $storeValue = $branchStore[$onlyIfStoreKey] ?? null;
3812 22
                $storeValueAsBool = ($storeValue === null) ?
3813 22
                    true : (bool) Functions::flattenSingleValue($storeValue);
3814 22
                if (is_array($storeValue)) {
3815 18
                    $wrappedItem = end($storeValue);
3816 18
                    $storeValue = end($wrappedItem);
3817
                }
3818
3819 22
                if (isset($storeValue)
3820
                    && (
3821 21
                        !$storeValueAsBool
3822 21
                        || Functions::isError($storeValue)
3823 22
                        || ($storeValue === 'Pruned branch')
3824
                    )
3825
                ) {
3826
                    // If branching value is not true, we don't need to compute
3827 17
                    if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) {
3828 16
                        $stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token);
3829 16
                        $fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true;
3830
                    }
3831
3832 17
                    if (isset($storeKey)) {
3833
                        // We are processing an if condition
3834
                        // We cascade the pruning to the depending branches
3835 1
                        $branchStore[$storeKey] = 'Pruned branch';
3836 1
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
3837 1
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
3838
                    }
3839
3840 17
                    continue;
3841
                }
3842
            }
3843
3844 165
            if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) {
3845 21
                $onlyIfNotStoreKey = $tokenData['onlyIfNot'];
3846 21
                $storeValue = $branchStore[$onlyIfNotStoreKey] ?? null;
3847 21
                $storeValueAsBool = ($storeValue === null) ?
3848 21
                    true : (bool) Functions::flattenSingleValue($storeValue);
3849 21
                if (is_array($storeValue)) {
3850 17
                    $wrappedItem = end($storeValue);
3851 17
                    $storeValue = end($wrappedItem);
3852
                }
3853 21
                if (isset($storeValue)
3854
                    && (
3855 20
                        $storeValueAsBool
3856 20
                        || Functions::isError($storeValue)
3857 21
                        || ($storeValue === 'Pruned branch'))
3858
                ) {
3859
                    // If branching value is true, we don't need to compute
3860 19
                    if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) {
3861 19
                        $stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token);
3862 19
                        $fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true;
3863
                    }
3864
3865 19
                    if (isset($storeKey)) {
3866
                        // We are processing an if condition
3867
                        // We cascade the pruning to the depending branches
3868 2
                        $branchStore[$storeKey] = 'Pruned branch';
3869 2
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
3870 2
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
3871
                    }
3872
3873 19
                    continue;
3874
                }
3875
            }
3876
3877
            // 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
3878 165
            if (isset(self::$binaryOperators[$token])) {
3879
                //    We must have two operands, error if we don't
3880 154
                if (($operand2Data = $stack->pop()) === null) {
3881
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3882
                }
3883 154
                if (($operand1Data = $stack->pop()) === null) {
3884
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3885
                }
3886
3887 154
                $operand1 = self::dataTestReference($operand1Data);
3888 154
                $operand2 = self::dataTestReference($operand2Data);
3889
3890
                //    Log what we're doing
3891 154
                if ($token == ':') {
3892 55
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
3893
                } else {
3894 135
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
3895
                }
3896
3897
                //    Process the operation in the appropriate manner
3898
                switch ($token) {
3899
                    //    Comparison (Boolean) Operators
3900 154
                    case '>':            //    Greater than
3901 138
                    case '<':            //    Less than
3902 126
                    case '>=':            //    Greater than or Equal to
3903 117
                    case '<=':            //    Less than or Equal to
3904 106
                    case '=':            //    Equality
3905 77
                    case '<>':            //    Inequality
3906 112
                        $result = $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
3907 112
                        if (isset($storeKey)) {
3908 19
                            $branchStore[$storeKey] = $result;
3909
                        }
3910
3911 112
                        break;
3912
                    //    Binary Operators
3913 69
                    case ':':            //    Range
3914 55
                        if (strpos($operand1Data['reference'], '!') !== false) {
3915 55
                            [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
3916
                        } else {
3917 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

3917
                            $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...
3918
                        }
3919
3920 55
                        [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true);
3921 55
                        if (empty($sheet2)) {
3922 10
                            $sheet2 = $sheet1;
3923
                        }
3924
3925 55
                        if ($sheet1 == $sheet2) {
3926 55
                            if ($operand1Data['reference'] === null) {
3927
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
3928
                                    $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

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

4142
                            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...
4143 58
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4144 58
                                $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

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

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