Failed Conditions
Push — master ( a2bb82...a189d9 )
by Adrien
10:27 queued 01:00
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\Arabic::class, 'evaluate'],
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\Distributions\Binomial::class, 'distribution'],
422
            'argumentCount' => '4',
423
        ],
424
        'BINOM.DIST' => [
425
            'category' => Category::CATEGORY_STATISTICAL,
426
            'functionCall' => [Statistical\Distributions\Binomial::class, 'distribution'],
427
            'argumentCount' => '4',
428
        ],
429
        'BINOM.DIST.RANGE' => [
430
            'category' => Category::CATEGORY_STATISTICAL,
431
            'functionCall' => [Statistical\Distributions\Binomial::class, 'range'],
432
            'argumentCount' => '3,4',
433
        ],
434
        'BINOM.INV' => [
435
            'category' => Category::CATEGORY_STATISTICAL,
436
            'functionCall' => [Statistical\Distributions\Binomial::class, 'inverse'],
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, 'distributionRightTail'],
492
            'argumentCount' => '2',
493
        ],
494
        'CHISQ.DIST' => [
495
            'category' => Category::CATEGORY_STATISTICAL,
496
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionLeftTail'],
497
            'argumentCount' => '3',
498
        ],
499
        'CHISQ.DIST.RT' => [
500
            'category' => Category::CATEGORY_STATISTICAL,
501
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionRightTail'],
502
            'argumentCount' => '2',
503
        ],
504
        'CHIINV' => [
505
            'category' => Category::CATEGORY_STATISTICAL,
506
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'],
507
            'argumentCount' => '2',
508
        ],
509
        'CHISQ.INV' => [
510
            'category' => Category::CATEGORY_STATISTICAL,
511
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseLeftTail'],
512
            'argumentCount' => '2',
513
        ],
514
        'CHISQ.INV.RT' => [
515
            'category' => Category::CATEGORY_STATISTICAL,
516
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'],
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\Combinations::class, 'withoutRepetition'],
559
            'argumentCount' => '2',
560
        ],
561
        'COMBINA' => [
562
            'category' => Category::CATEGORY_MATH_AND_TRIG,
563
            'functionCall' => [MathTrig\Combinations::class, 'withRepetition'],
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\Distributions\Binomial::class, 'inverse'],
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\Distributions\Exponential::class, 'distribution'],
984
            'argumentCount' => '3',
985
        ],
986
        'EXPON.DIST' => [
987
            'category' => Category::CATEGORY_STATISTICAL,
988
            'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'],
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\FactDouble::class, 'evaluate'],
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\Distributions\F::class, 'distribution'],
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\Gcd::class, 'evaluate'],
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\Distributions\HyperGeometric::class, 'distribution'],
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\Logarithms::class, 'natural'],
1570
            'argumentCount' => '1',
1571
        ],
1572
        'LOG' => [
1573
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1574
            'functionCall' => [MathTrig\Logarithms::class, 'withBase'],
1575
            'argumentCount' => '1,2',
1576
        ],
1577
        'LOG10' => [
1578
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1579
            'functionCall' => [MathTrig\Logarithms::class, 'base10'],
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\Distributions\LogNormal::class, 'inverse'],
1590
            'argumentCount' => '3',
1591
        ],
1592
        'LOGNORMDIST' => [
1593
            'category' => Category::CATEGORY_STATISTICAL,
1594
            'functionCall' => [Statistical\Distributions\LogNormal::class, 'cumulative'],
1595
            'argumentCount' => '3',
1596
        ],
1597
        'LOGNORM.DIST' => [
1598
            'category' => Category::CATEGORY_STATISTICAL,
1599
            'functionCall' => [Statistical\Distributions\LogNormal::class, 'distribution'],
1600
            'argumentCount' => '4',
1601
        ],
1602
        'LOGNORM.INV' => [
1603
            'category' => Category::CATEGORY_STATISTICAL,
1604
            'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'],
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\Mod::class, 'evaluate'],
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' => [MathTrig\MatrixFunctions::class, 'funcMUnit'],
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\Distributions\Binomial::class, 'negative'],
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\Distributions\Normal::class, 'distribution'],
1780
            'argumentCount' => '4',
1781
        ],
1782
        'NORM.DIST' => [
1783
            'category' => Category::CATEGORY_STATISTICAL,
1784
            'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'],
1785
            'argumentCount' => '4',
1786
        ],
1787
        'NORMINV' => [
1788
            'category' => Category::CATEGORY_STATISTICAL,
1789
            'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'],
1790
            'argumentCount' => '3',
1791
        ],
1792
        'NORM.INV' => [
1793
            'category' => Category::CATEGORY_STATISTICAL,
1794
            'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'],
1795
            'argumentCount' => '3',
1796
        ],
1797
        'NORMSDIST' => [
1798
            'category' => Category::CATEGORY_STATISTICAL,
1799
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'cumulative'],
1800
            'argumentCount' => '1',
1801
        ],
1802
        'NORM.S.DIST' => [
1803
            'category' => Category::CATEGORY_STATISTICAL,
1804
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'distribution'],
1805
            'argumentCount' => '1,2',
1806
        ],
1807
        'NORMSINV' => [
1808
            'category' => Category::CATEGORY_STATISTICAL,
1809
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'],
1810
            'argumentCount' => '1',
1811
        ],
1812
        'NORM.S.INV' => [
1813
            'category' => Category::CATEGORY_STATISTICAL,
1814
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'],
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\Percentiles::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\Percentiles::class, 'PERCENTILE'],
1917
            'argumentCount' => '2',
1918
        ],
1919
        'PERCENTRANK' => [
1920
            'category' => Category::CATEGORY_STATISTICAL,
1921
            'functionCall' => [Statistical\Percentiles::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\Percentiles::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\Power::class, 'evaluate'],
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\Percentiles::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\Percentiles::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\Random::class, 'randNoArgs'],
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\Random::class, 'randBetween'],
2057
            'argumentCount' => '2',
2058
        ],
2059
        'RANK' => [
2060
            'category' => Category::CATEGORY_STATISTICAL,
2061
            'functionCall' => [Statistical\Percentiles::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\Distributions\StandardNormal::class, 'zTest'],
2655
            'argumentCount' => '2-3',
2656
        ],
2657
        'Z.TEST' => [
2658
            'category' => Category::CATEGORY_STATISTICAL,
2659
            'functionCall' => [Statistical\Distributions\StandardNormal::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 3865
    public function __construct(?Spreadsheet $spreadsheet = null)
2681
    {
2682 3865
        $this->delta = 1 * 10 ** (0 - ini_get('precision'));
2683
2684 3865
        $this->spreadsheet = $spreadsheet;
2685 3865
        $this->cyclicReferenceStack = new CyclicReferenceStack();
2686 3865
        $this->debugLog = new Logger($this->cyclicReferenceStack);
2687 3865
        self::$referenceHelper = ReferenceHelper::getInstance();
2688 3865
    }
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 4292
    public static function getInstance(?Spreadsheet $spreadsheet = null): self
2708
    {
2709 4292
        if ($spreadsheet !== null) {
2710 3520
            $instance = $spreadsheet->getCalculationEngine();
2711 3520
            if (isset($instance)) {
2712 3520
                return $instance;
2713
            }
2714
        }
2715
2716 842
        if (!isset(self::$instance) || (self::$instance === null)) {
2717 14
            self::$instance = new self();
2718
        }
2719
2720 842
        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 366
    public function getDebugLog()
2739
    {
2740 366
        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 175
    public static function setArrayReturnType($returnType)
2779
    {
2780
        if (
2781 175
            ($returnType == self::RETURN_ARRAY_AS_VALUE) ||
2782 175
            ($returnType == self::RETURN_ARRAY_AS_ERROR) ||
2783 175
            ($returnType == self::RETURN_ARRAY_AS_ARRAY)
2784
        ) {
2785 175
            self::$returnArrayAsType = $returnType;
2786
2787 175
            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 175
    public static function getArrayReturnType()
2799
    {
2800 175
        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 30
    public function clearCalculationCacheForWorksheet($worksheetName): void
2854
    {
2855 30
        if (isset($this->calculationCache[$worksheetName])) {
2856
            unset($this->calculationCache[$worksheetName]);
2857
        }
2858 30
    }
2859
2860
    /**
2861
     * Rename calculation cache for a specified worksheet.
2862
     *
2863
     * @param string $fromWorksheetName
2864
     * @param string $toWorksheetName
2865
     */
2866 3865
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName): void
2867
    {
2868 3865
        if (isset($this->calculationCache[$fromWorksheetName])) {
2869
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2870
            unset($this->calculationCache[$fromWorksheetName]);
2871
        }
2872 3865
    }
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 3287
    public static function localeFunc($function)
3127
    {
3128 3287
        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 3287
        return $function;
3140
    }
3141
3142
    /**
3143
     * Wrap string values in quotes.
3144
     *
3145
     * @param mixed $value
3146
     *
3147
     * @return mixed
3148
     */
3149 3227
    public static function wrapResult($value)
3150
    {
3151 3227
        if (is_string($value)) {
3152
            //    Error values cannot be "wrapped"
3153 1781
            if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
3154
                //    Return Excel errors "as is"
3155 709
                return $value;
3156
            }
3157
3158
            //    Return strings wrapped in quotes
3159 1337
            return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
3160 2041
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
3161
            //    Convert numeric errors to NaN error
3162 3
            return Functions::NAN();
3163
        }
3164
3165 2038
        return $value;
3166
    }
3167
3168
    /**
3169
     * Remove quotes used as a wrapper to identify string values.
3170
     *
3171
     * @param mixed $value
3172
     *
3173
     * @return mixed
3174
     */
3175 3273
    public static function unwrapResult($value)
3176
    {
3177 3273
        if (is_string($value)) {
3178 1788
            if ((isset($value[0])) && ($value[0] == self::FORMULA_STRING_QUOTE) && (substr($value, -1) == self::FORMULA_STRING_QUOTE)) {
3179 1788
                return substr($value, 1, -1);
3180
            }
3181
            //    Convert numeric errors to NAN error
3182 3010
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
3183
            return Functions::NAN();
3184
        }
3185
3186 3142
        return $value;
3187
    }
3188
3189
    /**
3190
     * Calculate cell value (using formula from a cell ID)
3191
     * Retained for backward compatibility.
3192
     *
3193
     * @param Cell $pCell Cell to calculate
3194
     *
3195
     * @return mixed
3196
     */
3197
    public function calculate(?Cell $pCell = null)
3198
    {
3199
        try {
3200
            return $this->calculateCellValue($pCell);
3201
        } catch (\Exception $e) {
3202
            throw new Exception($e->getMessage());
3203
        }
3204
    }
3205
3206
    /**
3207
     * Calculate the value of a cell formula.
3208
     *
3209
     * @param Cell $pCell Cell to calculate
3210
     * @param bool $resetLog Flag indicating whether the debug log should be reset or not
3211
     *
3212
     * @return mixed
3213
     */
3214 3253
    public function calculateCellValue(?Cell $pCell = null, $resetLog = true)
3215
    {
3216 3253
        if ($pCell === null) {
3217
            return null;
3218
        }
3219
3220 3253
        $returnArrayAsType = self::$returnArrayAsType;
3221 3253
        if ($resetLog) {
3222
            //    Initialise the logging settings if requested
3223 3251
            $this->formulaError = null;
3224 3251
            $this->debugLog->clearLog();
3225 3251
            $this->cyclicReferenceStack->clear();
3226 3251
            $this->cyclicFormulaCounter = 1;
3227
3228 3251
            self::$returnArrayAsType = self::RETURN_ARRAY_AS_ARRAY;
3229
        }
3230
3231
        //    Execute the calculation for the cell formula
3232 3253
        $this->cellStack[] = [
3233 3253
            'sheet' => $pCell->getWorksheet()->getTitle(),
3234 3253
            'cell' => $pCell->getCoordinate(),
3235
        ];
3236
3237
        try {
3238 3253
            $result = self::unwrapResult($this->_calculateFormulaValue($pCell->getValue(), $pCell->getCoordinate(), $pCell));
3239 3136
            $cellAddress = array_pop($this->cellStack);
3240 3136
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
3241 122
        } catch (\Exception $e) {
3242 122
            $cellAddress = array_pop($this->cellStack);
3243 122
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
3244
3245 122
            throw new Exception($e->getMessage());
3246
        }
3247
3248 3136
        if ((is_array($result)) && (self::$returnArrayAsType != self::RETURN_ARRAY_AS_ARRAY)) {
3249 2
            self::$returnArrayAsType = $returnArrayAsType;
3250 2
            $testResult = Functions::flattenArray($result);
3251 2
            if (self::$returnArrayAsType == self::RETURN_ARRAY_AS_ERROR) {
3252
                return Functions::VALUE();
3253
            }
3254
            //    If there's only a single cell in the array, then we allow it
3255 2
            if (count($testResult) != 1) {
3256
                //    If keys are numeric, then it's a matrix result rather than a cell range result, so we permit it
3257
                $r = array_keys($result);
3258
                $r = array_shift($r);
3259
                if (!is_numeric($r)) {
3260
                    return Functions::VALUE();
3261
                }
3262
                if (is_array($result[$r])) {
3263
                    $c = array_keys($result[$r]);
3264
                    $c = array_shift($c);
3265
                    if (!is_numeric($c)) {
3266
                        return Functions::VALUE();
3267
                    }
3268
                }
3269
            }
3270 2
            $result = array_shift($testResult);
3271
        }
3272 3136
        self::$returnArrayAsType = $returnArrayAsType;
3273
3274 3136
        if ($result === null && $pCell->getWorksheet()->getSheetView()->getShowZeros()) {
3275 5
            return 0;
3276 3136
        } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
3277
            return Functions::NAN();
3278
        }
3279
3280 3136
        return $result;
3281
    }
3282
3283
    /**
3284
     * Validate and parse a formula string.
3285
     *
3286
     * @param string $formula Formula to parse
3287
     *
3288
     * @return array|bool
3289
     */
3290 6
    public function parseFormula($formula)
3291
    {
3292
        //    Basic validation that this is indeed a formula
3293
        //    We return an empty array if not
3294 6
        $formula = trim($formula);
3295 6
        if ((!isset($formula[0])) || ($formula[0] != '=')) {
3296
            return [];
3297
        }
3298 6
        $formula = ltrim(substr($formula, 1));
3299 6
        if (!isset($formula[0])) {
3300
            return [];
3301
        }
3302
3303
        //    Parse the formula and return the token stack
3304 6
        return $this->internalParseFormula($formula);
3305
    }
3306
3307
    /**
3308
     * Calculate the value of a formula.
3309
     *
3310
     * @param string $formula Formula to parse
3311
     * @param string $cellID Address of the cell to calculate
3312
     * @param Cell $pCell Cell to calculate
3313
     *
3314
     * @return mixed
3315
     */
3316 1
    public function calculateFormula($formula, $cellID = null, ?Cell $pCell = null)
3317
    {
3318
        //    Initialise the logging settings
3319 1
        $this->formulaError = null;
3320 1
        $this->debugLog->clearLog();
3321 1
        $this->cyclicReferenceStack->clear();
3322
3323 1
        $resetCache = $this->getCalculationCacheEnabled();
3324 1
        if ($this->spreadsheet !== null && $cellID === null && $pCell === null) {
3325
            $cellID = 'A1';
3326
            $pCell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
3327
        } else {
3328
            //    Disable calculation cacheing because it only applies to cell calculations, not straight formulae
3329
            //    But don't actually flush any cache
3330 1
            $this->calculationCacheEnabled = false;
3331
        }
3332
3333
        //    Execute the calculation
3334
        try {
3335 1
            $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $pCell));
3336 1
        } catch (\Exception $e) {
3337 1
            throw new Exception($e->getMessage());
3338
        }
3339
3340 1
        if ($this->spreadsheet === null) {
3341
            //    Reset calculation cacheing to its previous state
3342
            $this->calculationCacheEnabled = $resetCache;
3343
        }
3344
3345 1
        return $result;
3346
    }
3347
3348
    /**
3349
     * @param mixed $cellValue
3350
     */
3351 3252
    public function getValueFromCache(string $cellReference, &$cellValue): bool
3352
    {
3353 3252
        $this->debugLog->writeDebugLog("Testing cache value for cell {$cellReference}");
3354
        // Is calculation cacheing enabled?
3355
        // If so, is the required value present in calculation cache?
3356 3252
        if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
3357 81
            $this->debugLog->writeDebugLog("Retrieving value for cell {$cellReference} from cache");
3358
            // Return the cached result
3359
3360 81
            $cellValue = $this->calculationCache[$cellReference];
3361
3362 81
            return true;
3363
        }
3364
3365 3252
        return false;
3366
    }
3367
3368
    /**
3369
     * @param string $cellReference
3370
     * @param mixed $cellValue
3371
     */
3372 3135
    public function saveValueToCache($cellReference, $cellValue): void
3373
    {
3374 3135
        if ($this->calculationCacheEnabled) {
3375 3134
            $this->calculationCache[$cellReference] = $cellValue;
3376
        }
3377 3135
    }
3378
3379
    /**
3380
     * Parse a cell formula and calculate its value.
3381
     *
3382
     * @param string $formula The formula to parse and calculate
3383
     * @param string $cellID The ID (e.g. A3) of the cell that we are calculating
3384
     * @param Cell $pCell Cell to calculate
3385
     *
3386
     * @return mixed
3387
     */
3388 3417
    public function _calculateFormulaValue($formula, $cellID = null, ?Cell $pCell = null)
3389
    {
3390 3417
        $cellValue = null;
3391
3392
        //  Quote-Prefixed cell values cannot be formulae, but are treated as strings
3393 3417
        if ($pCell !== null && $pCell->getStyle()->getQuotePrefix() === true) {
3394 1
            return self::wrapResult((string) $formula);
3395
        }
3396
3397 3417
        if (preg_match('/^=\s*cmd\s*\|/miu', $formula) !== 0) {
3398 1
            return self::wrapResult($formula);
3399
        }
3400
3401
        //    Basic validation that this is indeed a formula
3402
        //    We simply return the cell value if not
3403 3416
        $formula = trim($formula);
3404 3416
        if ($formula[0] != '=') {
3405 1
            return self::wrapResult($formula);
3406
        }
3407 3416
        $formula = ltrim(substr($formula, 1));
3408 3416
        if (!isset($formula[0])) {
3409 1
            return self::wrapResult($formula);
3410
        }
3411
3412 3415
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3413 3415
        $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
3414 3415
        $wsCellReference = $wsTitle . '!' . $cellID;
3415
3416 3415
        if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
3417 81
            return $cellValue;
3418
        }
3419 3415
        $this->debugLog->writeDebugLog("Evaluating formula for cell {$wsCellReference}");
3420
3421 3415
        if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) {
3422 3
            if ($this->cyclicFormulaCount <= 0) {
3423
                $this->cyclicFormulaCell = '';
3424
3425
                return $this->raiseFormulaError('Cyclic Reference in Formula');
3426 3
            } elseif ($this->cyclicFormulaCell === $wsCellReference) {
3427
                ++$this->cyclicFormulaCounter;
3428
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
3429
                    $this->cyclicFormulaCell = '';
3430
3431
                    return $cellValue;
3432
                }
3433 3
            } elseif ($this->cyclicFormulaCell == '') {
3434 3
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
3435 3
                    return $cellValue;
3436
                }
3437
                $this->cyclicFormulaCell = $wsCellReference;
3438
            }
3439
        }
3440
3441 3415
        $this->debugLog->writeDebugLog("Formula for cell {$wsCellReference} is {$formula}");
3442
        //    Parse the formula onto the token stack and calculate the value
3443 3415
        $this->cyclicReferenceStack->push($wsCellReference);
3444
3445 3415
        $cellValue = $this->processTokenStack($this->internalParseFormula($formula, $pCell), $cellID, $pCell);
3446 3298
        $this->cyclicReferenceStack->pop();
3447
3448
        // Save to calculation cache
3449 3298
        if ($cellID !== null) {
3450 3135
            $this->saveValueToCache($wsCellReference, $cellValue);
3451
        }
3452
3453
        //    Return the calculated value
3454 3298
        return $cellValue;
3455
    }
3456
3457
    /**
3458
     * Ensure that paired matrix operands are both matrices and of the same size.
3459
     *
3460
     * @param mixed &$operand1 First matrix operand
3461
     * @param mixed &$operand2 Second matrix operand
3462
     * @param int $resize Flag indicating whether the matrices should be resized to match
3463
     *                                        and (if so), whether the smaller dimension should grow or the
3464
     *                                        larger should shrink.
3465
     *                                            0 = no resize
3466
     *                                            1 = shrink to fit
3467
     *                                            2 = extend to fit
3468
     *
3469
     * @return array
3470
     */
3471 7
    private static function checkMatrixOperands(&$operand1, &$operand2, $resize = 1)
3472
    {
3473
        //    Examine each of the two operands, and turn them into an array if they aren't one already
3474
        //    Note that this function should only be called if one or both of the operand is already an array
3475 7
        if (!is_array($operand1)) {
3476
            [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand2);
3477
            $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1));
3478
            $resize = 0;
3479 7
        } elseif (!is_array($operand2)) {
3480 1
            [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand1);
3481 1
            $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2));
3482 1
            $resize = 0;
3483
        }
3484
3485 7
        [$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1);
3486 7
        [$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2);
3487 7
        if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
3488 7
            $resize = 1;
3489
        }
3490
3491 7
        if ($resize == 2) {
3492
            //    Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger
3493
            self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
3494 7
        } elseif ($resize == 1) {
3495
            //    Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller
3496 7
            self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
3497
        }
3498
3499 7
        return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns];
3500
    }
3501
3502
    /**
3503
     * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0.
3504
     *
3505
     * @param array &$matrix matrix operand
3506
     *
3507
     * @return int[] An array comprising the number of rows, and number of columns
3508
     */
3509 19
    public static function getMatrixDimensions(array &$matrix)
3510
    {
3511 19
        $matrixRows = count($matrix);
3512 19
        $matrixColumns = 0;
3513 19
        foreach ($matrix as $rowKey => $rowValue) {
3514 17
            if (!is_array($rowValue)) {
3515 4
                $matrix[$rowKey] = [$rowValue];
3516 4
                $matrixColumns = max(1, $matrixColumns);
3517
            } else {
3518 13
                $matrix[$rowKey] = array_values($rowValue);
3519 13
                $matrixColumns = max(count($rowValue), $matrixColumns);
3520
            }
3521
        }
3522 19
        $matrix = array_values($matrix);
3523
3524 19
        return [$matrixRows, $matrixColumns];
3525
    }
3526
3527
    /**
3528
     * Ensure that paired matrix operands are both matrices of the same size.
3529
     *
3530
     * @param mixed &$matrix1 First matrix operand
3531
     * @param mixed &$matrix2 Second matrix operand
3532
     * @param int $matrix1Rows Row size of first matrix operand
3533
     * @param int $matrix1Columns Column size of first matrix operand
3534
     * @param int $matrix2Rows Row size of second matrix operand
3535
     * @param int $matrix2Columns Column size of second matrix operand
3536
     */
3537 7
    private static function resizeMatricesShrink(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns): void
3538
    {
3539 7
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3540
            if ($matrix2Rows < $matrix1Rows) {
3541
                for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) {
3542
                    unset($matrix1[$i]);
3543
                }
3544
            }
3545
            if ($matrix2Columns < $matrix1Columns) {
3546
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3547
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3548
                        unset($matrix1[$i][$j]);
3549
                    }
3550
                }
3551
            }
3552
        }
3553
3554 7
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3555
            if ($matrix1Rows < $matrix2Rows) {
3556
                for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) {
3557
                    unset($matrix2[$i]);
3558
                }
3559
            }
3560
            if ($matrix1Columns < $matrix2Columns) {
3561
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3562
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3563
                        unset($matrix2[$i][$j]);
3564
                    }
3565
                }
3566
            }
3567
        }
3568 7
    }
3569
3570
    /**
3571
     * Ensure that paired matrix operands are both matrices of the same size.
3572
     *
3573
     * @param mixed &$matrix1 First matrix operand
3574
     * @param mixed &$matrix2 Second matrix operand
3575
     * @param int $matrix1Rows Row size of first matrix operand
3576
     * @param int $matrix1Columns Column size of first matrix operand
3577
     * @param int $matrix2Rows Row size of second matrix operand
3578
     * @param int $matrix2Columns Column size of second matrix operand
3579
     */
3580
    private static function resizeMatricesExtend(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns): void
3581
    {
3582
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3583
            if ($matrix2Columns < $matrix1Columns) {
3584
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3585
                    $x = $matrix2[$i][$matrix2Columns - 1];
3586
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3587
                        $matrix2[$i][$j] = $x;
3588
                    }
3589
                }
3590
            }
3591
            if ($matrix2Rows < $matrix1Rows) {
3592
                $x = $matrix2[$matrix2Rows - 1];
3593
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3594
                    $matrix2[$i] = $x;
3595
                }
3596
            }
3597
        }
3598
3599
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3600
            if ($matrix1Columns < $matrix2Columns) {
3601
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3602
                    $x = $matrix1[$i][$matrix1Columns - 1];
3603
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3604
                        $matrix1[$i][$j] = $x;
3605
                    }
3606
                }
3607
            }
3608
            if ($matrix1Rows < $matrix2Rows) {
3609
                $x = $matrix1[$matrix1Rows - 1];
3610
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3611
                    $matrix1[$i] = $x;
3612
                }
3613
            }
3614
        }
3615
    }
3616
3617
    /**
3618
     * Format details of an operand for display in the log (based on operand type).
3619
     *
3620
     * @param mixed $value First matrix operand
3621
     *
3622
     * @return mixed
3623
     */
3624 3293
    private function showValue($value)
3625
    {
3626 3293
        if ($this->debugLog->getWriteDebugLog()) {
3627
            $testArray = Functions::flattenArray($value);
3628
            if (count($testArray) == 1) {
3629
                $value = array_pop($testArray);
3630
            }
3631
3632
            if (is_array($value)) {
3633
                $returnMatrix = [];
3634
                $pad = $rpad = ', ';
3635
                foreach ($value as $row) {
3636
                    if (is_array($row)) {
3637
                        $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
3638
                        $rpad = '; ';
3639
                    } else {
3640
                        $returnMatrix[] = $this->showValue($row);
3641
                    }
3642
                }
3643
3644
                return '{ ' . implode($rpad, $returnMatrix) . ' }';
3645
            } elseif (is_string($value) && (trim($value, self::FORMULA_STRING_QUOTE) == $value)) {
3646
                return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
3647
            } elseif (is_bool($value)) {
3648
                return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3649
            }
3650
        }
3651
3652 3293
        return Functions::flattenSingleValue($value);
3653
    }
3654
3655
    /**
3656
     * Format type and details of an operand for display in the log (based on operand type).
3657
     *
3658
     * @param mixed $value First matrix operand
3659
     *
3660
     * @return null|string
3661
     */
3662 3296
    private function showTypeDetails($value)
3663
    {
3664 3296
        if ($this->debugLog->getWriteDebugLog()) {
3665
            $testArray = Functions::flattenArray($value);
3666
            if (count($testArray) == 1) {
3667
                $value = array_pop($testArray);
3668
            }
3669
3670
            if ($value === null) {
3671
                return 'a NULL value';
3672
            } elseif (is_float($value)) {
3673
                $typeString = 'a floating point number';
3674
            } elseif (is_int($value)) {
3675
                $typeString = 'an integer number';
3676
            } elseif (is_bool($value)) {
3677
                $typeString = 'a boolean';
3678
            } elseif (is_array($value)) {
3679
                $typeString = 'a matrix';
3680
            } else {
3681
                if ($value == '') {
3682
                    return 'an empty string';
3683
                } elseif ($value[0] == '#') {
3684
                    return 'a ' . $value . ' error';
3685
                }
3686
                $typeString = 'a string';
3687
            }
3688
3689
            return $typeString . ' with a value of ' . $this->showValue($value);
3690
        }
3691 3296
    }
3692
3693
    /**
3694
     * @param string $formula
3695
     *
3696
     * @return false|string False indicates an error
3697
     */
3698 3421
    private function convertMatrixReferences($formula)
3699
    {
3700 3421
        static $matrixReplaceFrom = [self::FORMULA_OPEN_FUNCTION_BRACE, ';', self::FORMULA_CLOSE_FUNCTION_BRACE];
3701 3421
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
3702
3703
        //    Convert any Excel matrix references to the MKMATRIX() function
3704 3421
        if (strpos($formula, self::FORMULA_OPEN_FUNCTION_BRACE) !== false) {
3705
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
3706 17
            if (strpos($formula, self::FORMULA_STRING_QUOTE) !== false) {
3707
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
3708
                //        the formula
3709 3
                $temp = explode(self::FORMULA_STRING_QUOTE, $formula);
3710
                //    Open and Closed counts used for trapping mismatched braces in the formula
3711 3
                $openCount = $closeCount = 0;
3712 3
                $i = false;
3713 3
                foreach ($temp as &$value) {
3714
                    //    Only count/replace in alternating array entries
3715 3
                    if ($i = !$i) {
0 ignored issues
show
introduced by
The condition $i is always false.
Loading history...
3716 3
                        $openCount += substr_count($value, self::FORMULA_OPEN_FUNCTION_BRACE);
3717 3
                        $closeCount += substr_count($value, self::FORMULA_CLOSE_FUNCTION_BRACE);
3718 3
                        $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
3719
                    }
3720
                }
3721 3
                unset($value);
3722
                //    Then rebuild the formula string
3723 3
                $formula = implode(self::FORMULA_STRING_QUOTE, $temp);
3724
            } else {
3725
                //    If there's no quoted strings, then we do a simple count/replace
3726 15
                $openCount = substr_count($formula, self::FORMULA_OPEN_FUNCTION_BRACE);
3727 15
                $closeCount = substr_count($formula, self::FORMULA_CLOSE_FUNCTION_BRACE);
3728 15
                $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
3729
            }
3730
            //    Trap for mismatched braces and trigger an appropriate error
3731 17
            if ($openCount < $closeCount) {
3732
                if ($openCount > 0) {
3733
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
3734
                }
3735
3736
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
3737 17
            } elseif ($openCount > $closeCount) {
3738
                if ($closeCount > 0) {
3739
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
3740
                }
3741
3742
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
3743
            }
3744
        }
3745
3746 3421
        return $formula;
3747
    }
3748
3749
    //    Binary Operators
3750
    //    These operators always work on two values
3751
    //    Array key is the operator, the value indicates whether this is a left or right associative operator
3752
    private static $operatorAssociativity = [
3753
        '^' => 0, //    Exponentiation
3754
        '*' => 0, '/' => 0, //    Multiplication and Division
3755
        '+' => 0, '-' => 0, //    Addition and Subtraction
3756
        '&' => 0, //    Concatenation
3757
        '|' => 0, ':' => 0, //    Intersect and Range
3758
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3759
    ];
3760
3761
    //    Comparison (Boolean) Operators
3762
    //    These operators work on two values, but always return a boolean result
3763
    private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
3764
3765
    //    Operator Precedence
3766
    //    This list includes all valid operators, whether binary (including boolean) or unary (such as %)
3767
    //    Array key is the operator, the value is its precedence
3768
    private static $operatorPrecedence = [
3769
        ':' => 8, //    Range
3770
        '|' => 7, //    Intersect
3771
        '~' => 6, //    Negation
3772
        '%' => 5, //    Percentage
3773
        '^' => 4, //    Exponentiation
3774
        '*' => 3, '/' => 3, //    Multiplication and Division
3775
        '+' => 2, '-' => 2, //    Addition and Subtraction
3776
        '&' => 1, //    Concatenation
3777
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3778
    ];
3779
3780
    // Convert infix to postfix notation
3781
3782
    /**
3783
     * @param string $formula
3784
     *
3785
     * @return bool
3786
     */
3787 3421
    private function internalParseFormula($formula, ?Cell $pCell = null)
3788
    {
3789 3421
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
3790
            return false;
3791
        }
3792
3793
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
3794
        //        so we store the parent worksheet so that we can re-attach it when necessary
3795 3421
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3796
3797 3421
        $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION .
3798 3421
            '|' . self::CALCULATION_REGEXP_CELLREF .
3799 3421
            '|' . self::CALCULATION_REGEXP_NUMBER .
3800 3421
            '|' . self::CALCULATION_REGEXP_STRING .
3801 3421
            '|' . self::CALCULATION_REGEXP_OPENBRACE .
3802 3421
            '|' . self::CALCULATION_REGEXP_DEFINEDNAME .
3803 3421
            '|' . self::CALCULATION_REGEXP_ERROR .
3804 3421
            ')/sui';
3805
3806
        //    Start with initialisation
3807 3421
        $index = 0;
3808 3421
        $stack = new Stack();
3809 3421
        $output = [];
3810 3421
        $expectingOperator = false; //    We use this test in syntax-checking the expression to determine when a
3811
        //        - is a negation or + is a positive operator rather than an operation
3812 3421
        $expectingOperand = false; //    We use this test in syntax-checking the expression to determine whether an operand
3813
        //        should be null in a function call
3814
3815
        // IF branch pruning
3816
        // currently pending storeKey (last item of the storeKeysStack
3817 3421
        $pendingStoreKey = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $pendingStoreKey is dead and can be removed.
Loading history...
3818
        // stores a list of storeKeys (string[])
3819 3421
        $pendingStoreKeysStack = [];
3820 3421
        $expectingConditionMap = []; // ['storeKey' => true, ...]
3821 3421
        $expectingThenMap = []; // ['storeKey' => true, ...]
3822 3421
        $expectingElseMap = []; // ['storeKey' => true, ...]
3823 3421
        $parenthesisDepthMap = []; // ['storeKey' => 4, ...]
3824
3825
        //    The guts of the lexical parser
3826
        //    Loop through the formula extracting each operator and operand in turn
3827 3421
        while (true) {
3828
            // Branch pruning: we adapt the output item to the context (it will
3829
            // be used to limit its computation)
3830 3421
            $currentCondition = null;
3831 3421
            $currentOnlyIf = null;
3832 3421
            $currentOnlyIfNot = null;
3833 3421
            $previousStoreKey = null;
3834 3421
            $pendingStoreKey = end($pendingStoreKeysStack);
3835
3836 3421
            if ($this->branchPruningEnabled) {
3837
                // this is a condition ?
3838 3421
                if (isset($expectingConditionMap[$pendingStoreKey]) && $expectingConditionMap[$pendingStoreKey]) {
3839 34
                    $currentCondition = $pendingStoreKey;
3840 34
                    $stackDepth = count($pendingStoreKeysStack);
3841 34
                    if ($stackDepth > 1) { // nested if
3842 7
                        $previousStoreKey = $pendingStoreKeysStack[$stackDepth - 2];
3843
                    }
3844
                }
3845 3421
                if (isset($expectingThenMap[$pendingStoreKey]) && $expectingThenMap[$pendingStoreKey]) {
3846 34
                    $currentOnlyIf = $pendingStoreKey;
3847 3421
                } elseif (isset($previousStoreKey)) {
3848 7
                    if (isset($expectingThenMap[$previousStoreKey]) && $expectingThenMap[$previousStoreKey]) {
3849 2
                        $currentOnlyIf = $previousStoreKey;
3850
                    }
3851
                }
3852 3421
                if (isset($expectingElseMap[$pendingStoreKey]) && $expectingElseMap[$pendingStoreKey]) {
3853 31
                    $currentOnlyIfNot = $pendingStoreKey;
3854 3421
                } elseif (isset($previousStoreKey)) {
3855 7
                    if (isset($expectingElseMap[$previousStoreKey]) && $expectingElseMap[$previousStoreKey]) {
3856 5
                        $currentOnlyIfNot = $previousStoreKey;
3857
                    }
3858
                }
3859
            }
3860
3861 3421
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
3862
3863 3421
            if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
3864 47
                $opCharacter .= $formula[++$index];
3865
            }
3866
            //    Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
3867 3421
            $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
3868 3421
            if ($opCharacter == '-' && !$expectingOperator) {                //    Is it a negation instead of a minus?
3869
                //    Put a negation on the stack
3870 301
                $stack->push('Unary Operator', '~', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3871 301
                ++$index; //        and drop the negation symbol
3872 3421
            } elseif ($opCharacter == '%' && $expectingOperator) {
3873
                //    Put a percentage on the stack
3874 2
                $stack->push('Unary Operator', '%', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3875 2
                ++$index;
3876 3421
            } elseif ($opCharacter == '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
3877
                ++$index; //    Drop the redundant plus symbol
3878 3421
            } elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) {    //    We have to explicitly deny a tilde or pipe, because they are legal
3879
                return $this->raiseFormulaError("Formula Error: Illegal character '~'"); //        on the stack but not in the input expression
3880 3421
            } elseif ((isset(self::$operators[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) {    //    Are we putting an operator on the stack?
3881
                while (
3882 524
                    $stack->count() > 0 &&
3883 524
                    ($o2 = $stack->last()) &&
3884 524
                    isset(self::$operators[$o2['value']]) &&
3885 524
                    @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])
3886
                ) {
3887 24
                    $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3888
                }
3889
3890
                //    Finally put our current operator onto the stack
3891 524
                $stack->push('Binary Operator', $opCharacter, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3892
3893 524
                ++$index;
3894 524
                $expectingOperator = false;
3895 3421
            } elseif ($opCharacter == ')' && $expectingOperator) {            //    Are we expecting to close a parenthesis?
3896 3287
                $expectingOperand = false;
3897 3287
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3898 578
                    if ($o2 === null) {
3899
                        return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"');
3900
                    }
3901 578
                    $output[] = $o2;
3902
                }
3903 3287
                $d = $stack->last(2);
3904
3905
                // Branch pruning we decrease the depth whether is it a function
3906
                // call or a parenthesis
3907 3287
                if (!empty($pendingStoreKey)) {
3908 34
                    --$parenthesisDepthMap[$pendingStoreKey];
3909
                }
3910
3911 3287
                if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) {    //    Did this parenthesis just close a function?
3912 3286
                    if (!empty($pendingStoreKey) && $parenthesisDepthMap[$pendingStoreKey] == -1) {
3913
                        // we are closing an IF(
3914 34
                        if ($d['value'] != 'IF(') {
3915
                            return $this->raiseFormulaError('Parser bug we should be in an "IF("');
3916
                        }
3917 34
                        if ($expectingConditionMap[$pendingStoreKey]) {
3918
                            return $this->raiseFormulaError('We should not be expecting a condition');
3919
                        }
3920 34
                        $expectingThenMap[$pendingStoreKey] = false;
3921 34
                        $expectingElseMap[$pendingStoreKey] = false;
3922 34
                        --$parenthesisDepthMap[$pendingStoreKey];
3923 34
                        array_pop($pendingStoreKeysStack);
3924 34
                        unset($pendingStoreKey);
3925
                    }
3926
3927 3286
                    $functionName = $matches[1]; //    Get the function name
3928 3286
                    $d = $stack->pop();
3929 3286
                    $argumentCount = $d['value']; //    See how many arguments there were (argument count is the next value stored on the stack)
3930 3286
                    $output[] = $d; //    Dump the argument count on the output
3931 3286
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
3932 3286
                    if (isset(self::$controlFunctions[$functionName])) {
3933 32
                        $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
3934 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...
3935 3277
                    } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3936 3277
                        $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
3937 3277
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3938
                    } else {    // did we somehow push a non-function on the stack? this should never happen
3939
                        return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack');
3940
                    }
3941
                    //    Check the argument count
3942 3286
                    $argumentCountError = false;
3943 3286
                    $expectedArgumentCountString = null;
3944 3286
                    if (is_numeric($expectedArgumentCount)) {
3945 1589
                        if ($expectedArgumentCount < 0) {
3946 4
                            if ($argumentCount > abs($expectedArgumentCount)) {
3947
                                $argumentCountError = true;
3948 4
                                $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
3949
                            }
3950
                        } else {
3951 1585
                            if ($argumentCount != $expectedArgumentCount) {
3952 77
                                $argumentCountError = true;
3953 1589
                                $expectedArgumentCountString = $expectedArgumentCount;
3954
                            }
3955
                        }
3956 1740
                    } elseif ($expectedArgumentCount != '*') {
3957 1728
                        $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...
3958 1728
                        switch ($argMatch[2]) {
3959 1728
                            case '+':
3960 299
                                if ($argumentCount < $argMatch[1]) {
3961 3
                                    $argumentCountError = true;
3962 3
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
3963
                                }
3964
3965 299
                                break;
3966 1461
                            case '-':
3967 237
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
3968 7
                                    $argumentCountError = true;
3969 7
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
3970
                                }
3971
3972 237
                                break;
3973 1224
                            case ',':
3974 1224
                                if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
3975 24
                                    $argumentCountError = true;
3976 24
                                    $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
3977
                                }
3978
3979 1224
                                break;
3980
                        }
3981
                    }
3982 3286
                    if ($argumentCountError) {
3983 111
                        return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected');
3984
                    }
3985
                }
3986 3176
                ++$index;
3987 3421
            } elseif ($opCharacter == ',') {            //    Is this the separator for function arguments?
3988
                if (
3989 1776
                    !empty($pendingStoreKey) &&
3990 1776
                    $parenthesisDepthMap[$pendingStoreKey] == 0
3991
                ) {
3992
                    // We must go to the IF next argument
3993 34
                    if ($expectingConditionMap[$pendingStoreKey]) {
3994 34
                        $expectingConditionMap[$pendingStoreKey] = false;
3995 34
                        $expectingThenMap[$pendingStoreKey] = true;
3996 31
                    } elseif ($expectingThenMap[$pendingStoreKey]) {
3997 31
                        $expectingThenMap[$pendingStoreKey] = false;
3998 31
                        $expectingElseMap[$pendingStoreKey] = true;
3999
                    } elseif ($expectingElseMap[$pendingStoreKey]) {
4000
                        return $this->raiseFormulaError('Reaching fourth argument of an IF');
4001
                    }
4002
                }
4003 1776
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
4004 226
                    if ($o2 === null) {
4005
                        return $this->raiseFormulaError('Formula Error: Unexpected ,');
4006
                    }
4007 226
                    $output[] = $o2; // pop the argument expression stuff and push onto the output
4008
                }
4009
                //    If we've a comma when we're expecting an operand, then what we actually have is a null operand;
4010
                //        so push a null onto the stack
4011 1776
                if (($expectingOperand) || (!$expectingOperator)) {
4012 25
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
4013
                }
4014
                // make sure there was a function
4015 1776
                $d = $stack->last(2);
4016 1776
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) {
4017
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
4018
                }
4019 1776
                $d = $stack->pop();
4020 1776
                $itemStoreKey = $d['storeKey'] ?? null;
4021 1776
                $itemOnlyIf = $d['onlyIf'] ?? null;
4022 1776
                $itemOnlyIfNot = $d['onlyIfNot'] ?? null;
4023 1776
                $stack->push($d['type'], ++$d['value'], $d['reference'], $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // increment the argument count
4024 1776
                $stack->push('Brace', '(', null, $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // put the ( back on, we'll need to pop back to it again
4025 1776
                $expectingOperator = false;
4026 1776
                $expectingOperand = true;
4027 1776
                ++$index;
4028 3421
            } elseif ($opCharacter == '(' && !$expectingOperator) {
4029 5
                if (!empty($pendingStoreKey)) { // Branch pruning: we go deeper
4030 1
                    ++$parenthesisDepthMap[$pendingStoreKey];
4031
                }
4032 5
                $stack->push('Brace', '(', null, $currentCondition, $currentOnlyIf, $currentOnlyIf);
4033 5
                ++$index;
4034 3421
            } elseif ($isOperandOrFunction && !$expectingOperator) {    // do we now have a function/variable/number?
4035 3421
                $expectingOperator = true;
4036 3421
                $expectingOperand = false;
4037 3421
                $val = $match[1];
4038 3421
                $length = strlen($val);
4039 3421
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) {
4040 3287
                    $val = preg_replace('/\s/u', '', $val);
4041 3287
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
4042 3285
                        $valToUpper = strtoupper($val);
4043
                    } else {
4044 3
                        $valToUpper = 'NAME.ERROR(';
4045
                    }
4046
                    // here $matches[1] will contain values like "IF"
4047
                    // and $val "IF("
4048 3287
                    if ($this->branchPruningEnabled && ($valToUpper == 'IF(')) { // we handle a new if
4049 34
                        $pendingStoreKey = $this->getUnusedBranchStoreKey();
4050 34
                        $pendingStoreKeysStack[] = $pendingStoreKey;
4051 34
                        $expectingConditionMap[$pendingStoreKey] = true;
4052 34
                        $parenthesisDepthMap[$pendingStoreKey] = 0;
4053
                    } else { // this is not an if but we go deeper
4054 3279
                        if (!empty($pendingStoreKey) && array_key_exists($pendingStoreKey, $parenthesisDepthMap)) {
4055 9
                            ++$parenthesisDepthMap[$pendingStoreKey];
4056
                        }
4057
                    }
4058
4059 3287
                    $stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4060
                    // tests if the function is closed right after opening
4061 3287
                    $ax = preg_match('/^\s*\)/u', substr($formula, $index + $length));
4062 3287
                    if ($ax) {
4063 160
                        $stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4064 160
                        $expectingOperator = true;
4065
                    } else {
4066 3196
                        $stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4067 3196
                        $expectingOperator = false;
4068
                    }
4069 3287
                    $stack->push('Brace', '(');
4070 3313
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
4071
                    //    Watch for this case-change when modifying to allow cell references in different worksheets...
4072
                    //    Should only be applied to the actual cell column, not the worksheet name
4073
                    //    If the last entry on the stack was a : operator, then we have a cell range reference
4074 1082
                    $testPrevOp = $stack->last(1);
4075 1082
                    if ($testPrevOp !== null && $testPrevOp['value'] == ':') {
4076
                        //    If we have a worksheet reference, then we're playing with a 3D reference
4077 298
                        if ($matches[2] == '') {
4078
                            //    Otherwise, we 'inherit' the worksheet reference from the start cell reference
4079
                            //    The start of the cell range reference should be the last entry in $output
4080 295
                            $rangeStartCellRef = $output[count($output) - 1]['value'];
4081 295
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches);
4082 295
                            if ($rangeStartMatches[2] > '') {
4083 295
                                $val = $rangeStartMatches[2] . '!' . $val;
4084
                            }
4085
                        } else {
4086 3
                            $rangeStartCellRef = $output[count($output) - 1]['value'];
4087 3
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches);
4088 3
                            if ($rangeStartMatches[2] !== $matches[2]) {
4089 1
                                return $this->raiseFormulaError('3D Range references are not yet supported');
4090
                            }
4091
                        }
4092
                    }
4093
4094 1082
                    $outputItem = $stack->getStackItem('Cell Reference', $val, $val, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4095
4096 1082
                    $output[] = $outputItem;
4097
                } else {    // it's a variable, constant, string, number or boolean
4098
                    //    If the last entry on the stack was a : operator, then we may have a row or column range reference
4099 2491
                    $testPrevOp = $stack->last(1);
4100 2491
                    if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
4101 2
                        $startRowColRef = $output[count($output) - 1]['value'];
4102 2
                        [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
4103 2
                        $rangeSheetRef = $rangeWS1;
4104 2
                        if ($rangeWS1 != '') {
4105
                            $rangeWS1 .= '!';
4106
                        }
4107 2
                        [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
4108 2
                        if ($rangeWS2 != '') {
4109
                            $rangeWS2 .= '!';
4110
                        } else {
4111 2
                            $rangeWS2 = $rangeWS1;
4112
                        }
4113 2
                        $refSheet = $pCellParent;
4114 2
                        if ($pCellParent !== null && $rangeSheetRef !== $pCellParent->getTitle()) {
4115 2
                            $refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef);
4116
                        }
4117
                        if (
4118 2
                            (is_int($startRowColRef)) && (ctype_digit($val)) &&
4119 2
                            ($startRowColRef <= 1048576) && ($val <= 1048576)
4120
                        ) {
4121
                            //    Row range
4122 1
                            $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestColumn() : 'XFD'; //    Max 16,384 columns for Excel2007
4123 1
                            $output[count($output) - 1]['value'] = $rangeWS1 . 'A' . $startRowColRef;
4124 1
                            $val = $rangeWS2 . $endRowColRef . $val;
4125
                        } elseif (
4126 1
                            (ctype_alpha($startRowColRef)) && (ctype_alpha($val)) &&
4127 1
                            (strlen($startRowColRef) <= 3) && (strlen($val) <= 3)
4128
                        ) {
4129
                            //    Column range
4130
                            $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestRow() : 1048576; //    Max 1,048,576 rows for Excel2007
4131
                            $output[count($output) - 1]['value'] = $rangeWS1 . strtoupper($startRowColRef) . '1';
4132
                            $val = $rangeWS2 . $val . $endRowColRef;
4133
                        }
4134
                    }
4135
4136 2491
                    $localeConstant = false;
4137 2491
                    $stackItemType = 'Value';
4138 2491
                    $stackItemReference = null;
4139 2491
                    if ($opCharacter == self::FORMULA_STRING_QUOTE) {
4140
                        //    UnEscape any quotes within the string
4141 940
                        $val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
4142 1930
                    } elseif (is_numeric($val)) {
4143 1692
                        if ((strpos($val, '.') !== false) || (stripos($val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
4144 354
                            $val = (float) $val;
4145
                        } else {
4146 1692
                            $val = (int) $val;
4147
                        }
4148 312
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
4149 228
                        $stackItemType = 'Constant';
4150 228
                        $excelConstant = trim(strtoupper($val));
4151 228
                        $val = self::$excelConstants[$excelConstant];
4152 84
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
4153 17
                        $stackItemType = 'Constant';
4154 17
                        $val = self::$excelConstants[$localeConstant];
4155 67
                    } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', $val, $match)) {
4156 67
                        $stackItemType = 'Defined Name';
4157 67
                        $stackItemReference = $val;
4158
                    }
4159 2491
                    $details = $stack->getStackItem($stackItemType, $val, $stackItemReference, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4160 2491
                    if ($localeConstant) {
4161 17
                        $details['localeValue'] = $localeConstant;
4162
                    }
4163 2491
                    $output[] = $details;
4164
                }
4165 3421
                $index += $length;
4166 4
            } elseif ($opCharacter == '$') {    // absolute row or column range
4167
                ++$index;
4168 4
            } elseif ($opCharacter == ')') {    // miscellaneous error checking
4169 4
                if ($expectingOperand) {
4170 4
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
4171 4
                    $expectingOperand = false;
4172 4
                    $expectingOperator = true;
4173
                } else {
4174 4
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
4175
                }
4176
            } elseif (isset(self::$operators[$opCharacter]) && !$expectingOperator) {
4177
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
4178
            } else {    // I don't even want to know what you did to get here
4179
                return $this->raiseFormulaError('Formula Error: An unexpected error occurred');
4180
            }
4181
            //    Test for end of formula string
4182 3421
            if ($index == strlen($formula)) {
4183
                //    Did we end with an operator?.
4184
                //    Only valid for the % unary operator
4185 3309
                if ((isset(self::$operators[$opCharacter])) && ($opCharacter != '%')) {
4186
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
4187
                }
4188
4189 3309
                break;
4190
            }
4191
            //    Ignore white space
4192 3415
            while (($formula[$index] == "\n") || ($formula[$index] == "\r")) {
4193
                ++$index;
4194
            }
4195
4196 3415
            if ($formula[$index] == ' ') {
4197 1425
                while ($formula[$index] == ' ') {
4198 1425
                    ++$index;
4199
                }
4200
4201
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
4202
                //        Cell References) then we have an INTERSECTION operator
4203
                if (
4204 1425
                    ($expectingOperator) &&
4205 114
                    ((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
4206 114
                        ($output[count($output) - 1]['type'] == 'Cell Reference') ||
4207 104
                        (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', substr($formula, $index), $match)) &&
4208 1425
                        ($output[count($output) - 1]['type'] == 'Defined Name' || $output[count($output) - 1]['type'] == 'Value')
4209
                    )
4210
                ) {
4211
                    while (
4212 14
                        $stack->count() > 0 &&
4213 14
                        ($o2 = $stack->last()) &&
4214 14
                        isset(self::$operators[$o2['value']]) &&
4215 14
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])
4216
                    ) {
4217 10
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
4218
                    }
4219 14
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
4220 14
                    $expectingOperator = false;
4221
                }
4222
            }
4223
        }
4224
4225 3309
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
4226 207
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
4227
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
4228
            }
4229 207
            $output[] = $op;
4230
        }
4231
4232 3309
        return $output;
4233
    }
4234
4235 517
    private static function dataTestReference(&$operandData)
4236
    {
4237 517
        $operand = $operandData['value'];
4238 517
        if (($operandData['reference'] === null) && (is_array($operand))) {
4239 1
            $rKeys = array_keys($operand);
4240 1
            $rowKey = array_shift($rKeys);
4241 1
            $cKeys = array_keys(array_keys($operand[$rowKey]));
4242 1
            $colKey = array_shift($cKeys);
4243 1
            if (ctype_upper($colKey)) {
4244
                $operandData['reference'] = $colKey . $rowKey;
4245
            }
4246
        }
4247
4248 517
        return $operand;
4249
    }
4250
4251
    // evaluate postfix notation
4252
4253
    /**
4254
     * @param mixed $tokens
4255
     * @param null|string $cellID
4256
     *
4257
     * @return bool
4258
     */
4259 3303
    private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null)
4260
    {
4261 3303
        if ($tokens == false) {
4262
            return false;
4263
        }
4264
4265
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
4266
        //        so we store the parent cell collection so that we can re-attach it when necessary
4267 3303
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
4268 3303
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
4269 3303
        $stack = new Stack();
4270
4271
        // Stores branches that have been pruned
4272 3303
        $fakedForBranchPruning = [];
4273
        // help us to know when pruning ['branchTestId' => true/false]
4274 3303
        $branchStore = [];
4275
        //    Loop through each token in turn
4276 3303
        foreach ($tokens as $tokenData) {
4277 3303
            $token = $tokenData['value'];
4278
4279
            // Branch pruning: skip useless resolutions
4280 3303
            $storeKey = $tokenData['storeKey'] ?? null;
4281 3303
            if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) {
4282 29
                $onlyIfStoreKey = $tokenData['onlyIf'];
4283 29
                $storeValue = $branchStore[$onlyIfStoreKey] ?? null;
4284 29
                $storeValueAsBool = ($storeValue === null) ?
4285 29
                    true : (bool) Functions::flattenSingleValue($storeValue);
4286 29
                if (is_array($storeValue)) {
4287 24
                    $wrappedItem = end($storeValue);
4288 24
                    $storeValue = end($wrappedItem);
4289
                }
4290
4291
                if (
4292 29
                    isset($storeValue)
4293
                    && (
4294 28
                        !$storeValueAsBool
4295 28
                        || Functions::isError($storeValue)
4296 29
                        || ($storeValue === 'Pruned branch')
4297
                    )
4298
                ) {
4299
                    // If branching value is not true, we don't need to compute
4300 24
                    if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) {
4301 23
                        $stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token);
4302 23
                        $fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true;
4303
                    }
4304
4305 24
                    if (isset($storeKey)) {
4306
                        // We are processing an if condition
4307
                        // We cascade the pruning to the depending branches
4308 1
                        $branchStore[$storeKey] = 'Pruned branch';
4309 1
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4310 1
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4311
                    }
4312
4313 24
                    continue;
4314
                }
4315
            }
4316
4317 3303
            if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) {
4318 28
                $onlyIfNotStoreKey = $tokenData['onlyIfNot'];
4319 28
                $storeValue = $branchStore[$onlyIfNotStoreKey] ?? null;
4320 28
                $storeValueAsBool = ($storeValue === null) ?
4321 28
                    true : (bool) Functions::flattenSingleValue($storeValue);
4322 28
                if (is_array($storeValue)) {
4323 23
                    $wrappedItem = end($storeValue);
4324 23
                    $storeValue = end($wrappedItem);
4325
                }
4326
                if (
4327 28
                    isset($storeValue)
4328
                    && (
4329 27
                        $storeValueAsBool
4330 27
                        || Functions::isError($storeValue)
4331 28
                        || ($storeValue === 'Pruned branch'))
4332
                ) {
4333
                    // If branching value is true, we don't need to compute
4334 26
                    if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) {
4335 26
                        $stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token);
4336 26
                        $fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true;
4337
                    }
4338
4339 26
                    if (isset($storeKey)) {
4340
                        // We are processing an if condition
4341
                        // We cascade the pruning to the depending branches
4342 2
                        $branchStore[$storeKey] = 'Pruned branch';
4343 2
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4344 2
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4345
                    }
4346
4347 26
                    continue;
4348
                }
4349
            }
4350
4351
            // 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
4352 3303
            if (isset(self::$binaryOperators[$token])) {
4353
                //    We must have two operands, error if we don't
4354 517
                if (($operand2Data = $stack->pop()) === null) {
4355
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4356
                }
4357 517
                if (($operand1Data = $stack->pop()) === null) {
4358
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4359
                }
4360
4361 517
                $operand1 = self::dataTestReference($operand1Data);
4362 517
                $operand2 = self::dataTestReference($operand2Data);
4363
4364
                //    Log what we're doing
4365 517
                if ($token == ':') {
4366 293
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
4367
                } else {
4368 306
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
4369
                }
4370
4371
                //    Process the operation in the appropriate manner
4372
                switch ($token) {
4373
                    //    Comparison (Boolean) Operators
4374 517
                    case '>':            //    Greater than
4375 501
                    case '<':            //    Less than
4376 488
                    case '>=':            //    Greater than or Equal to
4377 480
                    case '<=':            //    Less than or Equal to
4378 470
                    case '=':            //    Equality
4379 389
                    case '<>':            //    Inequality
4380 183
                        $result = $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
4381 183
                        if (isset($storeKey)) {
4382 25
                            $branchStore[$storeKey] = $result;
4383
                        }
4384
4385 183
                        break;
4386
                    //    Binary Operators
4387 378
                    case ':':            //    Range
4388 293
                        if (strpos($operand1Data['reference'], '!') !== false) {
4389 288
                            [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
4390
                        } else {
4391 7
                            $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

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

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

4622
                            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...
4623 915
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4624 915
                                $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

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

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