Failed Conditions
Push — master ( 6a4138...48f8b5 )
by Adrien
11:09
created

Calculation::showValue()   B

Complexity

Conditions 10
Paths 15

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 10.0082

Importance

Changes 0
Metric Value
cc 10
eloc 19
c 0
b 0
f 0
nc 15
nop 1
dl 0
loc 29
ccs 22
cts 23
cp 0.9565
crap 10.0082
rs 7.6666

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

4368
                            $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...
4369
                        }
4370
4371
                        [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true);
4372
                        if (empty($sheet2)) {
4373
                            $sheet2 = $sheet1;
4374
                        }
4375 2
4376 2
                        if ($sheet1 == $sheet2) {
4377
                            if ($operand1Data['reference'] === null) {
4378 2
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
4379 2
                                    $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

4379
                                    $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...
4380 2
                                } elseif (trim($operand1Data['reference']) == '') {
4381 2
                                    $operand1Data['reference'] = $pCell->getCoordinate();
4382 2
                                } else {
4383
                                    $operand1Data['reference'] = $operand1Data['value'] . $pCell->getRow();
4384
                                }
4385
                            }
4386 20
                            if ($operand2Data['reference'] === null) {
4387
                                if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
4388 20
                                    $operand2Data['reference'] = $pCell->getColumn() . $operand2Data['value'];
4389
                                } elseif (trim($operand2Data['reference']) == '') {
4390 20
                                    $operand2Data['reference'] = $pCell->getCoordinate();
4391
                                } else {
4392
                                    $operand2Data['reference'] = $operand2Data['value'] . $pCell->getRow();
4393
                                }
4394 113
                            }
4395 56
4396
                            $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
4397 113
                            $oCol = $oRow = [];
4398 72
                            foreach ($oData as $oDatum) {
4399
                                $oCR = Coordinate::coordinateFromString($oDatum);
4400
                                $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
4401
                                $oRow[] = $oCR[1];
4402 113
                            }
4403 113
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4404 63
                            if ($pCellParent !== null) {
4405
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
4406 113
                            } else {
4407 74
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4408
                            }
4409
                            $stack->push('Cell Reference', $cellValue, $cellRef);
4410
                        } else {
4411 113
                            $stack->push('Error', Functions::REF(), null);
4412
                        }
4413
4414
                        break;
4415
                    case '+':            //    Addition
4416 113
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
4417 21
                        if (isset($storeKey)) {
4418 9
                            $branchStore[$storeKey] = $result;
4419
                        }
4420 21
4421
                        break;
4422
                    case '-':            //    Subtraction
4423 21
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
4424
                        if (isset($storeKey)) {
4425 96
                            $branchStore[$storeKey] = $result;
4426 13
                        }
4427 4
4428
                        break;
4429 13
                    case '*':            //    Multiplication
4430
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
4431
                        if (isset($storeKey)) {
4432 13
                            $branchStore[$storeKey] = $result;
4433
                        }
4434 84
4435 44
                        break;
4436 12
                    case '/':            //    Division
4437
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
4438 36
                        if (isset($storeKey)) {
4439
                            $branchStore[$storeKey] = $result;
4440
                        }
4441 44
4442
                        break;
4443 40
                    case '^':            //    Exponential
4444 9
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
4445 5
                        if (isset($storeKey)) {
4446 4
                            $branchStore[$storeKey] = $result;
4447 4
                        }
4448
4449 4
                        break;
4450
                    case '&':            //    Concatenation
4451
                        //    If either of the operands is a matrix, we need to treat them both as matrices
4452 9
                        //        (converting the other operand to a matrix if need be); then perform the required
4453
                        //        matrix operation
4454 31
                        if (is_bool($operand1)) {
4455 11
                            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
4456 7
                        }
4457 4
                        if (is_bool($operand2)) {
4458 4
                            $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
4459
                        }
4460 4
                        if ((is_array($operand1)) || (is_array($operand2))) {
4461
                            //    Ensure that both operands are arrays/matrices
4462
                            self::checkMatrixOperands($operand1, $operand2, 2);
4463 11
4464
                            try {
4465 20
                                //    Convert operand 1 from a PHP array to a matrix
4466 20
                                $matrix = new Shared\JAMA\Matrix($operand1);
4467 3
                                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4468
                                $matrixResult = $matrix->concat($operand2);
4469 17
                                $result = $matrixResult->getArray();
4470
                            } catch (\Exception $ex) {
4471
                                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4472 20
                                $result = '#VALUE!';
4473
                            }
4474
                        } else {
4475
                            $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE;
4476 113
                        }
4477
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
4478 113
                        $stack->push('Value', $result);
4479
4480 113
                        if (isset($storeKey)) {
4481
                            $branchStore[$storeKey] = $result;
4482
                        }
4483
4484
                        break;
4485
                    case '|':            //    Intersect
4486
                        $rowIntersect = array_intersect_key($operand1, $operand2);
4487
                        $cellIntersect = $oCol = $oRow = [];
4488
                        foreach (array_keys($rowIntersect) as $row) {
4489
                            $oRow[] = $row;
4490
                            foreach ($rowIntersect[$row] as $col => $data) {
4491 21
                                $oCol[] = Coordinate::columnIndexFromString($col) - 1;
4492
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
4493 21
                            }
4494 21
                        }
4495
                        if (count(Functions::flattenArray($cellIntersect)) === 0) {
4496 21
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
4497
                            $stack->push('Error', Functions::null(), null);
4498
                        } else {
4499
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' .
4500
                                Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4501
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
4502
                            $stack->push('Value', $cellIntersect, $cellRef);
4503
                        }
4504
4505
                        break;
4506
                }
4507
4508 40
                // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
4509
            } elseif (($token === '~') || ($token === '%')) {
4510
                if (($arg = $stack->pop()) === null) {
4511 40
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4512 2
                }
4513
                $arg = $arg['value'];
4514 39
                if ($token === '~') {
4515 1
                    $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg));
4516
                    $multiplier = -1;
4517
                } else {
4518
                    $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg));
4519
                    $multiplier = 0.01;
4520
                }
4521 39
                if (is_array($arg)) {
4522
                    self::checkMatrixOperands($arg, $multiplier, 2);
4523
4524
                    try {
4525
                        $matrix1 = new Shared\JAMA\Matrix($arg);
4526
                        $matrixResult = $matrix1->arrayTimesEquals($multiplier);
4527
                        $result = $matrixResult->getArray();
4528
                    } catch (\Exception $ex) {
4529
                        $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4530
                        $result = '#VALUE!';
4531
                    }
4532
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
4533
                    $stack->push('Value', $result);
4534
                    if (isset($storeKey)) {
4535
                        $branchStore[$storeKey] = $result;
4536 39
                    }
4537 39
                } else {
4538 39
                    $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack);
4539
                }
4540
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token, $matches)) {
4541
                $cellRef = null;
4542
                if (isset($matches[8])) {
4543
                    if ($pCell === null) {
4544 39
                        //                        We can't access the range, so return a REF error
4545 27
                        $cellValue = Functions::REF();
4546
                    } else {
4547 27
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
4548
                        if ($matches[2] > '') {
4549 30
                            $matches[2] = trim($matches[2], "\"'");
4550 7
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
4551
                                //    It's a Reference to an external spreadsheet (not currently supported)
4552 7
                                return $this->raiseFormulaError('Unable to access External Workbook');
4553
                            }
4554 28
                            $matches[2] = trim($matches[2], "\"'");
4555 23
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]);
4556
                            if ($pCellParent !== null) {
4557 23
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4558
                            } else {
4559 8
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4560 8
                            }
4561
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
4562 8
                        } else {
4563 8
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet');
4564
                            if ($pCellParent !== null) {
4565 8
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4566
                            } else {
4567
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4568
                            }
4569
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
4570
                        }
4571
                    }
4572
                } else {
4573
                    if ($pCell === null) {
4574
                        //                        We can't access the cell, so return a REF error
4575
                        $cellValue = Functions::REF();
4576
                    } else {
4577
                        $cellRef = $matches[6] . $matches[7];
4578
                        if ($matches[2] > '') {
4579
                            $matches[2] = trim($matches[2], "\"'");
4580 34
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
4581
                                //    It's a Reference to an external spreadsheet (not currently supported)
4582 34
                                return $this->raiseFormulaError('Unable to access External Workbook');
4583
                            }
4584 34
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]);
4585
                            if ($pCellParent !== null) {
4586
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
4587
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
4588
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4589
                                    $pCell->attach($pCellParent);
4590
                                } else {
4591
                                    $cellValue = null;
4592
                                }
4593
                            } else {
4594
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4595
                            }
4596
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
4597
                        } else {
4598
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet');
4599
                            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

4599
                            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...
4600
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4601
                                $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

4601
                                $pCell->attach(/** @scrutinizer ignore-type */ $pCellParent);
Loading history...
4602
                            } else {
4603
                                $cellValue = null;
4604
                            }
4605
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
4606
                        }
4607
                    }
4608
                }
4609 83
                $stack->push('Value', $cellValue, $cellRef);
4610
                if (isset($storeKey)) {
4611
                    $branchStore[$storeKey] = $cellValue;
4612 83
                }
4613
4614 83
                // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
4615 83
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token, $matches)) {
4616 83
                if ($pCellParent) {
4617
                    $pCell->attach($pCellParent);
4618
                }
4619
4620
                $functionName = $matches[1];
4621
                $argCount = $stack->pop();
4622 83
                $argCount = $argCount['value'];
4623 83
                if ($functionName != 'MKMATRIX') {
4624 83
                    $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's'));
4625 83
                }
4626 83
                if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) {    // function
4627
                    if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
4628 83
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
4629 83
                        $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
4630 83
                        $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
4631
                    } elseif (isset(self::$controlFunctions[$functionName])) {
4632 83
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
4633
                        $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
4634
                        $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
4635
                    }
4636 59
                    // get the arguments for this function
4637 59
                    $args = $argArrayVals = [];
4638 59
                    for ($i = 0; $i < $argCount; ++$i) {
4639
                        $arg = $stack->pop();
4640 59
                        $a = $argCount - $i - 1;
4641 59
                        if (
4642 59
                            ($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...
4643
                            (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) &&
4644 14
                            (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])
4645
                        ) {
4646
                            if ($arg['reference'] === null) {
4647
                                $args[] = $cellID;
4648
                                if ($functionName != 'MKMATRIX') {
4649
                                    $argArrayVals[] = $this->showValue($cellID);
4650 83
                                }
4651
                            } else {
4652
                                $args[] = $arg['reference'];
4653
                                if ($functionName != 'MKMATRIX') {
4654
                                    $argArrayVals[] = $this->showValue($arg['reference']);
4655
                                }
4656
                            }
4657
                        } else {
4658
                            $args[] = self::unwrapResult($arg['value']);
4659
                            if ($functionName != 'MKMATRIX') {
4660
                                $argArrayVals[] = $this->showValue($arg['value']);
4661
                            }
4662 10
                        }
4663
                    }
4664
4665 10
                    //    Reverse the order of the arguments
4666
                    krsort($args);
4667 10
4668 10
                    if (($passByReference) && ($argCount == 0)) {
4669 10
                        $args[] = $cellID;
4670
                        $argArrayVals[] = $this->showValue($cellID);
4671
                    }
4672
4673
                    if ($functionName != 'MKMATRIX') {
4674
                        if ($this->debugLog->getWriteDebugLog()) {
4675 10
                            krsort($argArrayVals);
4676 10
                            $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )');
4677 7
                        }
4678 7
                    }
4679 7
4680
                    //    Process the argument with the appropriate function call
4681 7
                    $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...
4682
4683 7
                    if (!is_array($functionCall)) {
4684 7
                        foreach ($args as &$arg) {
4685
                            $arg = Functions::flattenSingleValue($arg);
4686
                        }
4687 3
                        unset($arg);
4688
                    }
4689
4690
                    $result = call_user_func_array($functionCall, $args);
4691 7
4692 7
                    if ($functionName != 'MKMATRIX') {
4693
                        $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
4694 6
                    }
4695 6
                    $stack->push('Value', self::wrapResult($result));
4696 6
                    if (isset($storeKey)) {
4697
                        $branchStore[$storeKey] = $result;
4698 6
                    }
4699
                }
4700
            } else {
4701
                // if the token is a number, boolean, string or an Excel error, push it onto the stack
4702 1
                if (isset(self::$excelConstants[strtoupper($token)])) {
4703
                    $excelConstant = strtoupper($token);
4704 1
                    $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
4705 1
                    if (isset($storeKey)) {
4706 1
                        $branchStore[$storeKey] = self::$excelConstants[$excelConstant];
4707
                    }
4708
                    $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant]));
4709
                } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) {
4710
                    $stack->push('Value', $token);
4711
                    if (isset($storeKey)) {
4712
                        $branchStore[$storeKey] = $token;
4713
                    }
4714 7
                    // if the token is a named range, push the named range name onto the stack
4715
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/miu', $token, $matches)) {
4716
                    $namedRange = $matches[6];
4717
                    $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
4718
4719
                    $cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
4720
                    $pCell->attach($pCellParent);
4721
                    $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
4722
                    $stack->push('Named Range', $cellValue, $namedRange);
4723
                    if (isset($storeKey)) {
4724 3
                        $branchStore[$storeKey] = $cellValue;
4725
                    }
4726 3
                } else {
4727 3
                    return $this->raiseFormulaError("undefined variable '$token'");
4728
                }
4729 3
            }
4730
        }
4731
        // when we're out of tokens, the stack should have a single element, the final result
4732
        if ($stack->count() != 1) {
4733
            return $this->raiseFormulaError('internal error');
4734
        }
4735
        $output = $stack->pop();
4736
        $output = $output['value'];
4737
4738
        return $output;
4739
    }
4740
4741
    private function validateBinaryOperand(&$operand, &$stack)
4742
    {
4743
        if (is_array($operand)) {
4744
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
4745
                do {
4746
                    $operand = array_pop($operand);
4747 2
                } while (is_array($operand));
4748
            }
4749 2
        }
4750 2
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
4751 2
        if (is_string($operand)) {
4752 2
            //    We only need special validations for the operand if it is a string
4753
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
4754
            if ($operand > '' && $operand[0] == self::FORMULA_STRING_QUOTE) {
4755
                $operand = self::unwrapResult($operand);
4756 2
            }
4757
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
4758
            if (!is_numeric($operand)) {
4759
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
4760
                if ($operand > '' && $operand[0] == '#') {
4761
                    $stack->push('Value', $operand);
4762
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand));
4763
4764
                    return false;
4765
                } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
4766
                    //    If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
4767
                    $stack->push('Error', '#VALUE!');
4768
                    $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!'));
4769 80
4770
                    return false;
4771 80
                }
4772 4
            }
4773 4
        }
4774 4
4775
        //    return a true if the value of the operand is one that we can use in normal binary operations
4776 4
        return true;
4777 4
    }
4778 4
4779 1
    /**
4780
     * @param null|string $cellID
4781
     * @param mixed $operand1
4782
     * @param mixed $operand2
4783 4
     * @param string $operation
4784
     * @param bool $recursingArrays
4785
     *
4786 80
     * @return mixed
4787
     */
4788
    private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
4789 28
    {
4790
        //    If we're dealing with matrix operations, we want a matrix result
4791 28
        if ((is_array($operand1)) || (is_array($operand2))) {
4792 28
            $result = [];
4793
            if ((is_array($operand1)) && (!is_array($operand2))) {
4794 28
                foreach ($operand1 as $x => $operandData) {
4795
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2));
4796
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack);
4797
                    $r = $stack->pop();
4798
                    $result[$x] = $r['value'];
4799
                }
4800
            } elseif ((!is_array($operand1)) && (is_array($operand2))) {
4801
                foreach ($operand2 as $x => $operandData) {
4802
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData));
4803
                    $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack);
4804
                    $r = $stack->pop();
4805
                    $result[$x] = $r['value'];
4806
                }
4807
            } else {
4808
                if (!$recursingArrays) {
4809
                    self::checkMatrixOperands($operand1, $operand2, 2);
4810
                }
4811
                foreach ($operand1 as $x => $operandData) {
4812
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x]));
4813
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true);
4814
                    $r = $stack->pop();
4815
                    $result[$x] = $r['value'];
4816
                }
4817
            }
4818
            //    Log the result details
4819
            $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result));
4820
            //    And push the result onto the stack
4821
            $stack->push('Array', $result);
4822
4823
            return $result;
4824
        }
4825
4826
        //    Simple validate the two operands if they are string values
4827
        if (is_string($operand1) && $operand1 > '' && $operand1[0] == self::FORMULA_STRING_QUOTE) {
4828
            $operand1 = self::unwrapResult($operand1);
4829
        }
4830
        if (is_string($operand2) && $operand2 > '' && $operand2[0] == self::FORMULA_STRING_QUOTE) {
4831
            $operand2 = self::unwrapResult($operand2);
4832
        }
4833
4834
        // Use case insensitive comparaison if not OpenOffice mode
4835
        if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
4836
            if (is_string($operand1)) {
4837
                $operand1 = strtoupper($operand1);
4838
            }
4839
            if (is_string($operand2)) {
4840
                $operand2 = strtoupper($operand2);
4841
            }
4842
        }
4843
4844
        $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE;
4845
4846
        //    execute the necessary operation
4847
        switch ($operation) {
4848
            //    Greater than
4849
            case '>':
4850
                if ($useLowercaseFirstComparison) {
4851
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0;
4852
                } else {
4853
                    $result = ($operand1 > $operand2);
4854
                }
4855
4856
                break;
4857
            //    Less than
4858
            case '<':
4859
                if ($useLowercaseFirstComparison) {
4860
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0;
4861
                } else {
4862
                    $result = ($operand1 < $operand2);
4863
                }
4864
4865
                break;
4866
            //    Equality
4867
            case '=':
4868
                if (is_numeric($operand1) && is_numeric($operand2)) {
4869
                    $result = (abs($operand1 - $operand2) < $this->delta);
4870
                } else {
4871
                    $result = strcmp($operand1, $operand2) == 0;
4872
                }
4873
4874
                break;
4875
            //    Greater than or equal
4876
            case '>=':
4877
                if (is_numeric($operand1) && is_numeric($operand2)) {
4878
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2));
4879
                } elseif ($useLowercaseFirstComparison) {
4880
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0;
4881
                } else {
4882
                    $result = strcmp($operand1, $operand2) >= 0;
4883
                }
4884
4885
                break;
4886
            //    Less than or equal
4887
            case '<=':
4888
                if (is_numeric($operand1) && is_numeric($operand2)) {
4889
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2));
4890
                } elseif ($useLowercaseFirstComparison) {
4891
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0;
4892
                } else {
4893
                    $result = strcmp($operand1, $operand2) <= 0;
4894
                }
4895
4896
                break;
4897
            //    Inequality
4898
            case '<>':
4899
                if (is_numeric($operand1) && is_numeric($operand2)) {
4900
                    $result = (abs($operand1 - $operand2) > 1E-14);
4901
                } else {
4902
                    $result = strcmp($operand1, $operand2) != 0;
4903
                }
4904
4905
                break;
4906
        }
4907
4908
        //    Log the result details
4909
        $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...
4910
        //    And push the result onto the stack
4911
        $stack->push('Value', $result);
4912
4913
        return $result;
4914
    }
4915
4916
    /**
4917
     * Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
4918
     *
4919
     * @param string $str1 First string value for the comparison
4920
     * @param string $str2 Second string value for the comparison
4921
     *
4922
     * @return int
4923
     */
4924
    private function strcmpLowercaseFirst($str1, $str2)
4925
    {
4926
        $inversedStr1 = Shared\StringHelper::strCaseReverse($str1);
4927
        $inversedStr2 = Shared\StringHelper::strCaseReverse($str2);
4928
4929
        return strcmp($inversedStr1, $inversedStr2);
4930
    }
4931
4932
    /**
4933
     * @param mixed $operand1
4934
     * @param mixed $operand2
4935
     * @param mixed $operation
4936
     * @param string $matrixFunction
4937
     * @param mixed $stack
4938
     *
4939
     * @return bool|mixed
4940
     */
4941
    private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack)
4942
    {
4943
        //    Validate the two operands
4944
        if (!$this->validateBinaryOperand($operand1, $stack)) {
4945
            return false;
4946
        }
4947
        if (!$this->validateBinaryOperand($operand2, $stack)) {
4948
            return false;
4949
        }
4950
4951
        //    If either of the operands is a matrix, we need to treat them both as matrices
4952
        //        (converting the other operand to a matrix if need be); then perform the required
4953
        //        matrix operation
4954
        if ((is_array($operand1)) || (is_array($operand2))) {
4955
            //    Ensure that both operands are arrays/matrices of the same size
4956
            self::checkMatrixOperands($operand1, $operand2, 2);
4957
4958
            try {
4959
                //    Convert operand 1 from a PHP array to a matrix
4960
                $matrix = new Shared\JAMA\Matrix($operand1);
4961
                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4962
                $matrixResult = $matrix->$matrixFunction($operand2);
4963
                $result = $matrixResult->getArray();
4964
            } catch (\Exception $ex) {
4965
                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4966
                $result = '#VALUE!';
4967
            }
4968
        } else {
4969
            if (
4970
                (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) &&
4971
                ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) ||
4972
                    (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0))
4973
            ) {
4974
                $result = Functions::VALUE();
4975
            } else {
4976
                //    If we're dealing with non-matrix operations, execute the necessary operation
4977
                switch ($operation) {
4978
                    //    Addition
4979
                    case '+':
4980
                        $result = $operand1 + $operand2;
4981
4982
                        break;
4983
                    //    Subtraction
4984
                    case '-':
4985
                        $result = $operand1 - $operand2;
4986
4987
                        break;
4988
                    //    Multiplication
4989
                    case '*':
4990
                        $result = $operand1 * $operand2;
4991
4992
                        break;
4993
                    //    Division
4994
                    case '/':
4995
                        if ($operand2 == 0) {
4996
                            //    Trap for Divide by Zero error
4997
                            $stack->push('Error', '#DIV/0!');
4998
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails('#DIV/0!'));
4999
5000
                            return false;
5001
                        }
5002
                        $result = $operand1 / $operand2;
5003
5004
                        break;
5005
                    //    Power
5006
                    case '^':
5007
                        $result = $operand1 ** $operand2;
5008
5009
                        break;
5010
                }
5011
            }
5012
        }
5013
5014
        //    Log the result details
5015
        $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...
5016
        //    And push the result onto the stack
5017
        $stack->push('Value', $result);
5018
5019
        return $result;
5020
    }
5021
5022
    // trigger an error, but nicely, if need be
5023
    protected function raiseFormulaError($errorMessage)
5024
    {
5025
        $this->formulaError = $errorMessage;
5026
        $this->cyclicReferenceStack->clear();
5027
        if (!$this->suppressFormulaErrors) {
5028
            throw new Exception($errorMessage);
5029
        }
5030
        trigger_error($errorMessage, E_USER_ERROR);
5031
5032
        return false;
5033
    }
5034
5035
    /**
5036
     * Extract range values.
5037
     *
5038
     * @param string &$pRange String based range representation
5039
     * @param Worksheet $pSheet Worksheet
5040
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
5041
     *
5042
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
5043
     */
5044
    public function extractCellRange(&$pRange = 'A1', ?Worksheet $pSheet = null, $resetLog = true)
5045
    {
5046
        // Return value
5047
        $returnValue = [];
5048
5049
        if ($pSheet !== null) {
5050
            $pSheetName = $pSheet->getTitle();
5051
            if (strpos($pRange, '!') !== false) {
5052
                [$pSheetName, $pRange] = Worksheet::extractSheetTitle($pRange, true);
5053
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
5054
            }
5055
5056
            // Extract range
5057
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
5058
            $pRange = $pSheetName . '!' . $pRange;
5059
            if (!isset($aReferences[1])) {
5060
                $currentCol = '';
5061
                $currentRow = 0;
5062
                //    Single cell in range
5063
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
5064
                if ($pSheet->cellExists($aReferences[0])) {
5065
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5066
                } else {
5067
                    $returnValue[$currentRow][$currentCol] = null;
5068
                }
5069
            } else {
5070
                // Extract cell data for all cells in the range
5071
                foreach ($aReferences as $reference) {
5072
                    $currentCol = '';
5073
                    $currentRow = 0;
5074
                    // Extract range
5075
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
5076
                    if ($pSheet->cellExists($reference)) {
5077
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
5078
                    } else {
5079
                        $returnValue[$currentRow][$currentCol] = null;
5080
                    }
5081
                }
5082
            }
5083
        }
5084
5085
        return $returnValue;
5086
    }
5087
5088
    /**
5089
     * Extract range values.
5090
     *
5091
     * @param string &$pRange String based range representation
5092
     * @param Worksheet $pSheet Worksheet
5093
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
5094
     *
5095
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
5096
     */
5097
    public function extractNamedRange(&$pRange = 'A1', ?Worksheet $pSheet = null, $resetLog = true)
5098
    {
5099
        // Return value
5100
        $returnValue = [];
5101
5102
        if ($pSheet !== null) {
5103
            $pSheetName = $pSheet->getTitle();
0 ignored issues
show
Unused Code introduced by
The assignment to $pSheetName is dead and can be removed.
Loading history...
5104
            if (strpos($pRange, '!') !== false) {
5105
                [$pSheetName, $pRange] = Worksheet::extractSheetTitle($pRange, true);
5106
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
5107
            }
5108
5109
            // Named range?
5110
            $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

5110
            $namedRange = NamedRange::resolveRange($pRange, /** @scrutinizer ignore-type */ $pSheet);
Loading history...
5111
            if ($namedRange !== null) {
5112
                $pSheet = $namedRange->getWorksheet();
5113
                $pRange = $namedRange->getRange();
5114
                $splitRange = Coordinate::splitRange($pRange);
5115
                //    Convert row and column references
5116
                if (ctype_alpha($splitRange[0][0])) {
5117
                    $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
5118
                } elseif (ctype_digit($splitRange[0][0])) {
5119
                    $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
5120
                }
5121
            } else {
5122
                return Functions::REF();
5123
            }
5124
5125
            // Extract range
5126
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
5127
            if (!isset($aReferences[1])) {
5128
                //    Single cell (or single column or row) in range
5129
                [$currentCol, $currentRow] = Coordinate::coordinateFromString($aReferences[0]);
5130
                if ($pSheet->cellExists($aReferences[0])) {
5131
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5132
                } else {
5133
                    $returnValue[$currentRow][$currentCol] = null;
5134
                }
5135
            } else {
5136
                // Extract cell data for all cells in the range
5137
                foreach ($aReferences as $reference) {
5138
                    // Extract range
5139
                    [$currentCol, $currentRow] = Coordinate::coordinateFromString($reference);
5140
                    if ($pSheet->cellExists($reference)) {
5141
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
5142
                    } else {
5143
                        $returnValue[$currentRow][$currentCol] = null;
5144
                    }
5145
                }
5146
            }
5147
        }
5148
5149
        return $returnValue;
5150
    }
5151
5152
    /**
5153
     * Is a specific function implemented?
5154
     *
5155
     * @param string $pFunction Function Name
5156
     *
5157
     * @return bool
5158
     */
5159
    public function isImplemented($pFunction)
5160
    {
5161
        $pFunction = strtoupper($pFunction);
5162
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
5163
5164
        return !$notImplemented;
5165
    }
5166
5167
    /**
5168
     * Get a list of all implemented functions as an array of function objects.
5169
     *
5170
     * @return array of Category
5171
     */
5172
    public function getFunctions()
5173
    {
5174
        return self::$phpSpreadsheetFunctions;
5175
    }
5176
5177
    /**
5178
     * Get a list of implemented Excel function names.
5179
     *
5180
     * @return array
5181
     */
5182
    public function getImplementedFunctionNames()
5183
    {
5184
        $returnValue = [];
5185
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
5186
            if ($this->isImplemented($functionName)) {
5187
                $returnValue[] = $functionName;
5188
            }
5189
        }
5190
5191
        return $returnValue;
5192
    }
5193
5194
    /**
5195
     * Add cell reference if needed while making sure that it is the last argument.
5196
     *
5197
     * @param bool $passCellReference
5198
     * @param array|string $functionCall
5199
     *
5200
     * @return array
5201
     */
5202
    private function addCellReference(array $args, $passCellReference, $functionCall, ?Cell $pCell = null)
5203
    {
5204
        if ($passCellReference) {
5205
            if (is_array($functionCall)) {
5206
                $className = $functionCall[0];
5207
                $methodName = $functionCall[1];
5208
5209
                $reflectionMethod = new ReflectionMethod($className, $methodName);
5210
                $argumentCount = count($reflectionMethod->getParameters());
5211
                while (count($args) < $argumentCount - 1) {
5212
                    $args[] = null;
5213
                }
5214
            }
5215
5216
            $args[] = $pCell;
5217
        }
5218
5219
        return $args;
5220
    }
5221
5222
    private function getUnusedBranchStoreKey()
5223
    {
5224
        $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter;
5225
        ++$this->branchStoreKeyCounter;
5226
5227
        return $storeKeyValue;
5228
    }
5229
5230
    private function getTokensAsString($tokens)
5231
    {
5232
        $tokensStr = array_map(function ($token) {
5233
            $value = $token['value'] ?? 'no value';
5234
            while (is_array($value)) {
5235
                $value = array_pop($value);
5236
            }
5237
5238
            return $value;
5239
        }, $tokens);
5240
5241
        return '[ ' . implode(' | ', $tokensStr) . ' ]';
5242
    }
5243
}
5244