Failed Conditions
Push — master ( 67fec4...924347 )
by Adrien
07:48
created

Calculation::_calculateFormulaValue()   D

Complexity

Conditions 18
Paths 52

Size

Total Lines 67
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 20.0249

Importance

Changes 0
Metric Value
cc 18
eloc 37
c 0
b 0
f 0
nc 52
nop 3
dl 0
loc 67
rs 4.8666
ccs 31
cts 38
cp 0.8158
crap 20.0249

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

4389
                            $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...
4390
                        }
4391
4392 264
                        [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true);
4393 264
                        if (empty($sheet2)) {
4394 19
                            $sheet2 = $sheet1;
4395
                        }
4396
4397 264
                        if ($sheet1 == $sheet2) {
4398 264
                            if ($operand1Data['reference'] === null) {
4399
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
4400
                                    $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

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

4620
                            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...
4621 615
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4622 615
                                $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

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

5165
            $namedRange = DefinedName::resolveName($pRange, /** @scrutinizer ignore-type */ $pSheet);
Loading history...
5166
            if ($namedRange === null) {
5167
                return Functions::REF();
5168
            }
5169
5170
            $pSheet = $namedRange->getWorksheet();
5171
            $pRange = $namedRange->getValue();
5172
            $splitRange = Coordinate::splitRange($pRange);
5173
            //    Convert row and column references
5174
            if (ctype_alpha($splitRange[0][0])) {
5175
                $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
5176
            } elseif (ctype_digit($splitRange[0][0])) {
5177
                $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
5178
            }
5179
5180
            // Extract range
5181
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
5182
            if (!isset($aReferences[1])) {
5183
                //    Single cell (or single column or row) in range
5184
                [$currentCol, $currentRow] = Coordinate::coordinateFromString($aReferences[0]);
5185
                if ($pSheet->cellExists($aReferences[0])) {
5186
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5187
                } else {
5188
                    $returnValue[$currentRow][$currentCol] = null;
5189
                }
5190
            } else {
5191
                // Extract cell data for all cells in the range
5192
                foreach ($aReferences as $reference) {
5193
                    // Extract range
5194
                    [$currentCol, $currentRow] = Coordinate::coordinateFromString($reference);
5195
                    if ($pSheet->cellExists($reference)) {
5196
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
5197
                    } else {
5198
                        $returnValue[$currentRow][$currentCol] = null;
5199
                    }
5200
                }
5201
            }
5202
        }
5203
5204
        return $returnValue;
5205
    }
5206
5207
    /**
5208
     * Is a specific function implemented?
5209
     *
5210
     * @param string $pFunction Function Name
5211
     *
5212
     * @return bool
5213
     */
5214 3
    public function isImplemented($pFunction)
5215
    {
5216 3
        $pFunction = strtoupper($pFunction);
5217 3
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
5218
5219 3
        return !$notImplemented;
5220
    }
5221
5222
    /**
5223
     * Get a list of all implemented functions as an array of function objects.
5224
     *
5225
     * @return array of Category
5226
     */
5227
    public function getFunctions()
5228
    {
5229
        return self::$phpSpreadsheetFunctions;
5230
    }
5231
5232
    /**
5233
     * Get a list of implemented Excel function names.
5234
     *
5235
     * @return array
5236
     */
5237 2
    public function getImplementedFunctionNames()
5238
    {
5239 2
        $returnValue = [];
5240 2
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
5241 2
            if ($this->isImplemented($functionName)) {
5242 2
                $returnValue[] = $functionName;
5243
            }
5244
        }
5245
5246 2
        return $returnValue;
5247
    }
5248
5249
    /**
5250
     * Add cell reference if needed while making sure that it is the last argument.
5251
     *
5252
     * @param bool $passCellReference
5253
     * @param array|string $functionCall
5254
     *
5255
     * @return array
5256
     */
5257 2880
    private function addCellReference(array $args, $passCellReference, $functionCall, ?Cell $pCell = null)
5258
    {
5259 2880
        if ($passCellReference) {
5260 52
            if (is_array($functionCall)) {
5261 52
                $className = $functionCall[0];
5262 52
                $methodName = $functionCall[1];
5263
5264 52
                $reflectionMethod = new ReflectionMethod($className, $methodName);
5265 52
                $argumentCount = count($reflectionMethod->getParameters());
5266 52
                while (count($args) < $argumentCount - 1) {
5267 4
                    $args[] = null;
5268
                }
5269
            }
5270
5271 52
            $args[] = $pCell;
5272
        }
5273
5274 2880
        return $args;
5275
    }
5276
5277 34
    private function getUnusedBranchStoreKey()
5278
    {
5279 34
        $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter;
5280 34
        ++$this->branchStoreKeyCounter;
5281
5282 34
        return $storeKeyValue;
5283
    }
5284
5285
    private function getTokensAsString($tokens)
5286
    {
5287
        $tokensStr = array_map(function ($token) {
5288
            $value = $token['value'] ?? 'no value';
5289
            while (is_array($value)) {
5290
                $value = array_pop($value);
5291
            }
5292
5293
            return $value;
5294
        }, $tokens);
5295
5296
        return '[ ' . implode(' | ', $tokensStr) . ' ]';
5297
    }
5298
5299
    /**
5300
     * @return mixed|string
5301
     */
5302 64
    private function evaluateDefinedName(Cell $pCell, DefinedName $namedRange, Worksheet $pCellWorksheet, Stack $stack)
5303
    {
5304 64
        $definedNameScope = $namedRange->getScope();
5305 64
        if ($definedNameScope !== null && $definedNameScope !== $pCellWorksheet) {
5306
            // The defined name isn't in our current scope, so #REF
5307
            $result = Functions::REF();
5308
            $stack->push('Error', $result, $namedRange->getName());
5309
5310
            return $result;
5311
        }
5312
5313 64
        $definedNameValue = $namedRange->getValue();
5314 64
        $definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range';
5315 64
        $definedNameWorksheet = $namedRange->getWorksheet();
5316
5317 64
        if ($definedNameValue[0] !== '=') {
5318 53
            $definedNameValue = '=' . $definedNameValue;
5319
        }
5320
5321 64
        $this->debugLog->writeDebugLog("Defined Name is a {$definedNameType} with a value of {$definedNameValue}");
5322
5323 64
        $recursiveCalculationCell = ($definedNameWorksheet !== null && $definedNameWorksheet !== $pCellWorksheet)
5324 7
            ? $definedNameWorksheet->getCell('A1')
5325 64
            : $pCell;
5326 64
        $recursiveCalculationCellAddress = $recursiveCalculationCell !== null
5327 64
            ? $recursiveCalculationCell->getCoordinate()
5328 64
            : null;
5329
5330
        // Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns
5331 64
        $definedNameValue = self::$referenceHelper->updateFormulaReferencesAnyWorksheet(
5332
            $definedNameValue,
5333 64
            Coordinate::columnIndexFromString($pCell->getColumn()) - 1,
5334 64
            $pCell->getRow() - 1
5335
        );
5336
5337 64
        $this->debugLog->writeDebugLog("Value adjusted for relative references is {$definedNameValue}");
5338
5339 64
        $recursiveCalculator = new self($this->spreadsheet);
5340 64
        $recursiveCalculator->getDebugLog()->setWriteDebugLog($this->getDebugLog()->getWriteDebugLog());
5341 64
        $recursiveCalculator->getDebugLog()->setEchoDebugLog($this->getDebugLog()->getEchoDebugLog());
5342 64
        $result = $recursiveCalculator->_calculateFormulaValue($definedNameValue, $recursiveCalculationCellAddress, $recursiveCalculationCell);
5343
5344 64
        if ($this->getDebugLog()->getWriteDebugLog()) {
5345
            $this->debugLog->mergeDebugLog(array_slice($recursiveCalculator->getDebugLog()->getLog(), 3));
5346
            $this->debugLog->writeDebugLog("Evaluation Result for Named {$definedNameType} {$namedRange->getName()} is {$this->showTypeDetails($result)}");
5347
        }
5348
5349 64
        $stack->push('Defined Name', $result, $namedRange->getName());
5350
5351 64
        return $result;
5352
    }
5353
}
5354