Passed
Push — master ( a34695...33ec70 )
by Adrien
09:32
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 null|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 array<string, 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 array<string, mixed>
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\Absolute::class, 'evaluate'],
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\CashFlow\Constant\Periodic\Cumulative::class, 'interest'],
749
            'argumentCount' => '6',
750
        ],
751
        'CUMPRINC' => [
752
            'category' => Category::CATEGORY_FINANCIAL,
753
            'functionCall' => [Financial\CashFlow\Constant\Periodic\Cumulative::class, 'principal'],
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\Degrees::class, 'evaluate'],
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\Securities\Rates::class, 'discount'],
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\Exp::class, 'evaluate'],
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\CashFlow\Constant\Periodic::class, 'futureValue'],
1141
            'argumentCount' => '3-5',
1142
        ],
1143
        'FVSCHEDULE' => [
1144
            'category' => Category::CATEGORY_FINANCIAL,
1145
            'functionCall' => [Financial\CashFlow\Single::class, 'futureValue'],
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\Securities\Rates::class, 'interest'],
1433
            'argumentCount' => '4,5',
1434
        ],
1435
        'IPMT' => [
1436
            'category' => Category::CATEGORY_FINANCIAL,
1437
            'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'payment'],
1438
            'argumentCount' => '4-6',
1439
        ],
1440
        'IRR' => [
1441
            'category' => Category::CATEGORY_FINANCIAL,
1442
            'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'rate'],
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\CashFlow\Constant\Periodic\Interest::class, 'schedulePayment'],
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\CashFlow\Variable\Periodic::class, 'modifiedRate'],
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\CashFlow\Constant\Periodic::class, 'periods'],
1830
            'argumentCount' => '3-5',
1831
        ],
1832
        'NPV' => [
1833
            'category' => Category::CATEGORY_FINANCIAL,
1834
            'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'presentValue'],
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\CashFlow\Single::class, 'periods'],
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\CashFlow\Constant\Periodic\Payments::class, 'annuity'],
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\CashFlow\Constant\Periodic\Payments::class, 'interestPayment'],
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\CashFlow\Constant\Periodic::class, 'presentValue'],
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\Radians::class, 'evaluate'],
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\CashFlow\Constant\Periodic\Interest::class, 'rate'],
2077
            'argumentCount' => '3-6',
2078
        ],
2079
        'RECEIVED' => [
2080
            'category' => Category::CATEGORY_FINANCIAL,
2081
            'functionCall' => [Financial\Securities\Price::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\CashFlow\Single::class, 'interestRate'],
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\SeriesSum::class, 'funcSeriesSum'],
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\Sin::class, 'funcSin'],
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\Sqrt::class, 'evaluate'],
2254
            'argumentCount' => '1',
2255
        ],
2256
        'SQRTPI' => [
2257
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2258
            'functionCall' => [MathTrig\SqrtPi::class, 'evaluate'],
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\SumSquares::class, 'sumSquare'],
2335
            'argumentCount' => '1+',
2336
        ],
2337
        'SUMX2MY2' => [
2338
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2339
            'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredMinusYSquared'],
2340
            'argumentCount' => '2',
2341
        ],
2342
        'SUMX2PY2' => [
2343
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2344
            'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredPlusYSquared'],
2345
            'argumentCount' => '2',
2346
        ],
2347
        'SUMXMY2' => [
2348
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2349
            'functionCall' => [MathTrig\SumSquares::class, 'sumXMinusYSquared'],
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 4002
    public function __construct(?Spreadsheet $spreadsheet = null)
2681
    {
2682 4002
        $this->delta = 1 * 10 ** (0 - ini_get('precision'));
2683
2684 4002
        $this->spreadsheet = $spreadsheet;
2685 4002
        $this->cyclicReferenceStack = new CyclicReferenceStack();
2686 4002
        $this->debugLog = new Logger($this->cyclicReferenceStack);
2687 4002
        self::$referenceHelper = ReferenceHelper::getInstance();
2688 4002
    }
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 4422
    public static function getInstance(?Spreadsheet $spreadsheet = null): self
2708
    {
2709 4422
        if ($spreadsheet !== null) {
2710 3651
            $instance = $spreadsheet->getCalculationEngine();
2711 3651
            if (isset($instance)) {
2712 3651
                return $instance;
2713
            }
2714
        }
2715
2716 829
        if (!isset(self::$instance) || (self::$instance === null)) {
2717 14
            self::$instance = new self();
2718
        }
2719
2720 829
        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 368
    public function getDebugLog()
2739
    {
2740 368
        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 195
    public static function getTRUE(): string
2757
    {
2758 195
        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 179
    public static function getFALSE(): string
2767
    {
2768 179
        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 176
    public static function setArrayReturnType($returnType)
2779
    {
2780
        if (
2781 176
            ($returnType == self::RETURN_ARRAY_AS_VALUE) ||
2782 176
            ($returnType == self::RETURN_ARRAY_AS_ERROR) ||
2783 176
            ($returnType == self::RETURN_ARRAY_AS_ARRAY)
2784
        ) {
2785 176
            self::$returnArrayAsType = $returnType;
2786
2787 176
            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 176
    public static function getArrayReturnType()
2799
    {
2800 176
        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 17
    public function clearCalculationCacheForWorksheet($worksheetName): void
2854
    {
2855 17
        if (isset($this->calculationCache[$worksheetName])) {
2856
            unset($this->calculationCache[$worksheetName]);
2857
        }
2858 17
    }
2859
2860
    /**
2861
     * Rename calculation cache for a specified worksheet.
2862
     *
2863
     * @param string $fromWorksheetName
2864
     * @param string $toWorksheetName
2865
     */
2866 4002
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName): void
2867
    {
2868 4002
        if (isset($this->calculationCache[$fromWorksheetName])) {
2869
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2870
            unset($this->calculationCache[$fromWorksheetName]);
2871
        }
2872 4002
    }
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 3418
    public static function localeFunc($function)
3127
    {
3128 3418
        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 3418
        return $function;
3140
    }
3141
3142
    /**
3143
     * Wrap string values in quotes.
3144
     *
3145
     * @param mixed $value
3146
     *
3147
     * @return mixed
3148
     */
3149 3357
    public static function wrapResult($value)
3150
    {
3151 3357
        if (is_string($value)) {
3152
            //    Error values cannot be "wrapped"
3153 1805
            if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
3154
                //    Return Excel errors "as is"
3155 728
                return $value;
3156
            }
3157
3158
            //    Return strings wrapped in quotes
3159 1338
            return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
3160 2152
        } 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 2149
        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 3403
    public static function unwrapResult($value)
3176
    {
3177 3403
        if (is_string($value)) {
3178 1812
            if ((isset($value[0])) && ($value[0] == self::FORMULA_STRING_QUOTE) && (substr($value, -1) == self::FORMULA_STRING_QUOTE)) {
3179 1812
                return substr($value, 1, -1);
3180
            }
3181
            //    Convert numeric errors to NAN error
3182 3145
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
3183
            return Functions::NAN();
3184
        }
3185
3186 3272
        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 3383
    public function calculateCellValue(?Cell $pCell = null, $resetLog = true)
3215
    {
3216 3383
        if ($pCell === null) {
3217
            return null;
3218
        }
3219
3220 3383
        $returnArrayAsType = self::$returnArrayAsType;
3221 3383
        if ($resetLog) {
3222
            //    Initialise the logging settings if requested
3223 3381
            $this->formulaError = null;
3224 3381
            $this->debugLog->clearLog();
3225 3381
            $this->cyclicReferenceStack->clear();
3226 3381
            $this->cyclicFormulaCounter = 1;
3227
3228 3381
            self::$returnArrayAsType = self::RETURN_ARRAY_AS_ARRAY;
3229
        }
3230
3231
        //    Execute the calculation for the cell formula
3232 3383
        $this->cellStack[] = [
3233 3383
            'sheet' => $pCell->getWorksheet()->getTitle(),
3234 3383
            'cell' => $pCell->getCoordinate(),
3235
        ];
3236
3237
        try {
3238 3383
            $result = self::unwrapResult($this->_calculateFormulaValue($pCell->getValue(), $pCell->getCoordinate(), $pCell));
3239 3265
            $cellAddress = array_pop($this->cellStack);
3240 3265
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
3241 123
        } catch (\Exception $e) {
3242 123
            $cellAddress = array_pop($this->cellStack);
3243 123
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
3244
3245 123
            throw new Exception($e->getMessage());
3246
        }
3247
3248 3265
        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 3265
        self::$returnArrayAsType = $returnArrayAsType;
3273
3274 3265
        if ($result === null && $pCell->getWorksheet()->getSheetView()->getShowZeros()) {
3275 5
            return 0;
3276 3265
        } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
3277
            return Functions::NAN();
3278
        }
3279
3280 3265
        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 3382
    public function getValueFromCache(string $cellReference, &$cellValue): bool
3352
    {
3353 3382
        $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 3382
        if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
3357 87
            $this->debugLog->writeDebugLog("Retrieving value for cell {$cellReference} from cache");
3358
            // Return the cached result
3359
3360 87
            $cellValue = $this->calculationCache[$cellReference];
3361
3362 87
            return true;
3363
        }
3364
3365 3382
        return false;
3366
    }
3367
3368
    /**
3369
     * @param string $cellReference
3370
     * @param mixed $cellValue
3371
     */
3372 3264
    public function saveValueToCache($cellReference, $cellValue): void
3373
    {
3374 3264
        if ($this->calculationCacheEnabled) {
3375 3263
            $this->calculationCache[$cellReference] = $cellValue;
3376
        }
3377 3264
    }
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 3547
    public function _calculateFormulaValue($formula, $cellID = null, ?Cell $pCell = null)
3389
    {
3390 3547
        $cellValue = null;
3391
3392
        //  Quote-Prefixed cell values cannot be formulae, but are treated as strings
3393 3547
        if ($pCell !== null && $pCell->getStyle()->getQuotePrefix() === true) {
3394 1
            return self::wrapResult((string) $formula);
3395
        }
3396
3397 3547
        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 3546
        $formula = trim($formula);
3404 3546
        if ($formula[0] != '=') {
3405 1
            return self::wrapResult($formula);
3406
        }
3407 3546
        $formula = ltrim(substr($formula, 1));
3408 3546
        if (!isset($formula[0])) {
3409 1
            return self::wrapResult($formula);
3410
        }
3411
3412 3545
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3413 3545
        $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
3414 3545
        $wsCellReference = $wsTitle . '!' . $cellID;
3415
3416 3545
        if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
3417 87
            return $cellValue;
3418
        }
3419 3545
        $this->debugLog->writeDebugLog("Evaluating formula for cell {$wsCellReference}");
3420
3421 3545
        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 3545
        $this->debugLog->writeDebugLog("Formula for cell {$wsCellReference} is {$formula}");
3442
        //    Parse the formula onto the token stack and calculate the value
3443 3545
        $this->cyclicReferenceStack->push($wsCellReference);
3444
3445 3545
        $cellValue = $this->processTokenStack($this->internalParseFormula($formula, $pCell), $cellID, $pCell);
3446 3427
        $this->cyclicReferenceStack->pop();
3447
3448
        // Save to calculation cache
3449 3427
        if ($cellID !== null) {
3450 3264
            $this->saveValueToCache($wsCellReference, $cellValue);
3451
        }
3452
3453
        //    Return the calculated value
3454 3427
        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 3422
    private function showValue($value)
3625
    {
3626 3422
        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 3422
        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 3425
    private function showTypeDetails($value)
3663
    {
3664 3425
        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
3692 3425
        return null;
3693
    }
3694
3695
    /**
3696
     * @param string $formula
3697
     *
3698
     * @return false|string False indicates an error
3699
     */
3700 3551
    private function convertMatrixReferences($formula)
3701
    {
3702 3551
        static $matrixReplaceFrom = [self::FORMULA_OPEN_FUNCTION_BRACE, ';', self::FORMULA_CLOSE_FUNCTION_BRACE];
3703 3551
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
3704
3705
        //    Convert any Excel matrix references to the MKMATRIX() function
3706 3551
        if (strpos($formula, self::FORMULA_OPEN_FUNCTION_BRACE) !== false) {
3707
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
3708 17
            if (strpos($formula, self::FORMULA_STRING_QUOTE) !== false) {
3709
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
3710
                //        the formula
3711 3
                $temp = explode(self::FORMULA_STRING_QUOTE, $formula);
3712
                //    Open and Closed counts used for trapping mismatched braces in the formula
3713 3
                $openCount = $closeCount = 0;
3714 3
                $i = false;
3715 3
                foreach ($temp as &$value) {
3716
                    //    Only count/replace in alternating array entries
3717 3
                    if ($i = !$i) {
0 ignored issues
show
introduced by
The condition $i is always false.
Loading history...
3718 3
                        $openCount += substr_count($value, self::FORMULA_OPEN_FUNCTION_BRACE);
3719 3
                        $closeCount += substr_count($value, self::FORMULA_CLOSE_FUNCTION_BRACE);
3720 3
                        $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
3721
                    }
3722
                }
3723 3
                unset($value);
3724
                //    Then rebuild the formula string
3725 3
                $formula = implode(self::FORMULA_STRING_QUOTE, $temp);
3726
            } else {
3727
                //    If there's no quoted strings, then we do a simple count/replace
3728 15
                $openCount = substr_count($formula, self::FORMULA_OPEN_FUNCTION_BRACE);
3729 15
                $closeCount = substr_count($formula, self::FORMULA_CLOSE_FUNCTION_BRACE);
3730 15
                $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
3731
            }
3732
            //    Trap for mismatched braces and trigger an appropriate error
3733 17
            if ($openCount < $closeCount) {
3734
                if ($openCount > 0) {
3735
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
3736
                }
3737
3738
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
3739 17
            } elseif ($openCount > $closeCount) {
3740
                if ($closeCount > 0) {
3741
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
3742
                }
3743
3744
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
3745
            }
3746
        }
3747
3748 3551
        return $formula;
3749
    }
3750
3751
    //    Binary Operators
3752
    //    These operators always work on two values
3753
    //    Array key is the operator, the value indicates whether this is a left or right associative operator
3754
    private static $operatorAssociativity = [
3755
        '^' => 0, //    Exponentiation
3756
        '*' => 0, '/' => 0, //    Multiplication and Division
3757
        '+' => 0, '-' => 0, //    Addition and Subtraction
3758
        '&' => 0, //    Concatenation
3759
        '|' => 0, ':' => 0, //    Intersect and Range
3760
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3761
    ];
3762
3763
    //    Comparison (Boolean) Operators
3764
    //    These operators work on two values, but always return a boolean result
3765
    private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
3766
3767
    //    Operator Precedence
3768
    //    This list includes all valid operators, whether binary (including boolean) or unary (such as %)
3769
    //    Array key is the operator, the value is its precedence
3770
    private static $operatorPrecedence = [
3771
        ':' => 8, //    Range
3772
        '|' => 7, //    Intersect
3773
        '~' => 6, //    Negation
3774
        '%' => 5, //    Percentage
3775
        '^' => 4, //    Exponentiation
3776
        '*' => 3, '/' => 3, //    Multiplication and Division
3777
        '+' => 2, '-' => 2, //    Addition and Subtraction
3778
        '&' => 1, //    Concatenation
3779
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3780
    ];
3781
3782
    // Convert infix to postfix notation
3783
3784
    /**
3785
     * @param string $formula
3786
     *
3787
     * @return array<int, mixed>|false
3788
     */
3789 3551
    private function internalParseFormula($formula, ?Cell $pCell = null)
3790
    {
3791 3551
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
3792
            return false;
3793
        }
3794
3795
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
3796
        //        so we store the parent worksheet so that we can re-attach it when necessary
3797 3551
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3798
3799 3551
        $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION .
3800 3551
            '|' . self::CALCULATION_REGEXP_CELLREF .
3801 3551
            '|' . self::CALCULATION_REGEXP_NUMBER .
3802 3551
            '|' . self::CALCULATION_REGEXP_STRING .
3803 3551
            '|' . self::CALCULATION_REGEXP_OPENBRACE .
3804 3551
            '|' . self::CALCULATION_REGEXP_DEFINEDNAME .
3805 3551
            '|' . self::CALCULATION_REGEXP_ERROR .
3806 3551
            ')/sui';
3807
3808
        //    Start with initialisation
3809 3551
        $index = 0;
3810 3551
        $stack = new Stack();
3811 3551
        $output = [];
3812 3551
        $expectingOperator = false; //    We use this test in syntax-checking the expression to determine when a
3813
        //        - is a negation or + is a positive operator rather than an operation
3814 3551
        $expectingOperand = false; //    We use this test in syntax-checking the expression to determine whether an operand
3815
        //        should be null in a function call
3816
3817
        // IF branch pruning
3818
        // currently pending storeKey (last item of the storeKeysStack
3819 3551
        $pendingStoreKey = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $pendingStoreKey is dead and can be removed.
Loading history...
3820
        // stores a list of storeKeys (string[])
3821 3551
        $pendingStoreKeysStack = [];
3822 3551
        $expectingConditionMap = []; // ['storeKey' => true, ...]
3823 3551
        $expectingThenMap = []; // ['storeKey' => true, ...]
3824 3551
        $expectingElseMap = []; // ['storeKey' => true, ...]
3825 3551
        $parenthesisDepthMap = []; // ['storeKey' => 4, ...]
3826
3827
        //    The guts of the lexical parser
3828
        //    Loop through the formula extracting each operator and operand in turn
3829 3551
        while (true) {
3830
            // Branch pruning: we adapt the output item to the context (it will
3831
            // be used to limit its computation)
3832 3551
            $currentCondition = null;
3833 3551
            $currentOnlyIf = null;
3834 3551
            $currentOnlyIfNot = null;
3835 3551
            $previousStoreKey = null;
3836 3551
            $pendingStoreKey = end($pendingStoreKeysStack);
3837
3838 3551
            if ($this->branchPruningEnabled) {
3839
                // this is a condition ?
3840 3551
                if (isset($expectingConditionMap[$pendingStoreKey]) && $expectingConditionMap[$pendingStoreKey]) {
3841 34
                    $currentCondition = $pendingStoreKey;
3842 34
                    $stackDepth = count($pendingStoreKeysStack);
3843 34
                    if ($stackDepth > 1) { // nested if
3844 7
                        $previousStoreKey = $pendingStoreKeysStack[$stackDepth - 2];
3845
                    }
3846
                }
3847 3551
                if (isset($expectingThenMap[$pendingStoreKey]) && $expectingThenMap[$pendingStoreKey]) {
3848 34
                    $currentOnlyIf = $pendingStoreKey;
3849 3551
                } elseif (isset($previousStoreKey)) {
3850 7
                    if (isset($expectingThenMap[$previousStoreKey]) && $expectingThenMap[$previousStoreKey]) {
3851 2
                        $currentOnlyIf = $previousStoreKey;
3852
                    }
3853
                }
3854 3551
                if (isset($expectingElseMap[$pendingStoreKey]) && $expectingElseMap[$pendingStoreKey]) {
3855 31
                    $currentOnlyIfNot = $pendingStoreKey;
3856 3551
                } elseif (isset($previousStoreKey)) {
3857 7
                    if (isset($expectingElseMap[$previousStoreKey]) && $expectingElseMap[$previousStoreKey]) {
3858 5
                        $currentOnlyIfNot = $previousStoreKey;
3859
                    }
3860
                }
3861
            }
3862
3863 3551
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
3864
3865 3551
            if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
3866 47
                $opCharacter .= $formula[++$index];
3867
            }
3868
            //    Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
3869 3551
            $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
3870 3551
            if ($opCharacter == '-' && !$expectingOperator) {                //    Is it a negation instead of a minus?
3871
                //    Put a negation on the stack
3872 299
                $stack->push('Unary Operator', '~', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3873 299
                ++$index; //        and drop the negation symbol
3874 3551
            } elseif ($opCharacter == '%' && $expectingOperator) {
3875
                //    Put a percentage on the stack
3876 2
                $stack->push('Unary Operator', '%', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3877 2
                ++$index;
3878 3551
            } elseif ($opCharacter == '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
3879
                ++$index; //    Drop the redundant plus symbol
3880 3551
            } elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) {    //    We have to explicitly deny a tilde or pipe, because they are legal
3881
                return $this->raiseFormulaError("Formula Error: Illegal character '~'"); //        on the stack but not in the input expression
3882 3551
            } elseif ((isset(self::$operators[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) {    //    Are we putting an operator on the stack?
3883
                while (
3884 589
                    $stack->count() > 0 &&
3885 589
                    ($o2 = $stack->last()) &&
3886 589
                    isset(self::$operators[$o2['value']]) &&
3887 589
                    @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])
3888
                ) {
3889 24
                    $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3890
                }
3891
3892
                //    Finally put our current operator onto the stack
3893 589
                $stack->push('Binary Operator', $opCharacter, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3894
3895 589
                ++$index;
3896 589
                $expectingOperator = false;
3897 3551
            } elseif ($opCharacter == ')' && $expectingOperator) {            //    Are we expecting to close a parenthesis?
3898 3418
                $expectingOperand = false;
3899 3418
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3900 635
                    if ($o2 === null) {
3901
                        return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"');
3902
                    }
3903 635
                    $output[] = $o2;
3904
                }
3905 3418
                $d = $stack->last(2);
3906
3907
                // Branch pruning we decrease the depth whether is it a function
3908
                // call or a parenthesis
3909 3418
                if (!empty($pendingStoreKey)) {
3910 34
                    --$parenthesisDepthMap[$pendingStoreKey];
3911
                }
3912
3913 3418
                if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) {    //    Did this parenthesis just close a function?
3914 3417
                    if (!empty($pendingStoreKey) && $parenthesisDepthMap[$pendingStoreKey] == -1) {
3915
                        // we are closing an IF(
3916 34
                        if ($d['value'] != 'IF(') {
3917
                            return $this->raiseFormulaError('Parser bug we should be in an "IF("');
3918
                        }
3919 34
                        if ($expectingConditionMap[$pendingStoreKey]) {
3920
                            return $this->raiseFormulaError('We should not be expecting a condition');
3921
                        }
3922 34
                        $expectingThenMap[$pendingStoreKey] = false;
3923 34
                        $expectingElseMap[$pendingStoreKey] = false;
3924 34
                        --$parenthesisDepthMap[$pendingStoreKey];
3925 34
                        array_pop($pendingStoreKeysStack);
3926 34
                        unset($pendingStoreKey);
3927
                    }
3928
3929 3417
                    $functionName = $matches[1]; //    Get the function name
3930 3417
                    $d = $stack->pop();
3931 3417
                    $argumentCount = $d['value']; //    See how many arguments there were (argument count is the next value stored on the stack)
3932 3417
                    $output[] = $d; //    Dump the argument count on the output
3933 3417
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
3934 3417
                    if (isset(self::$controlFunctions[$functionName])) {
3935 32
                        $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
3936 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...
3937 3408
                    } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3938 3408
                        $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
3939 3408
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3940
                    } else {    // did we somehow push a non-function on the stack? this should never happen
3941
                        return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack');
3942
                    }
3943
                    //    Check the argument count
3944 3417
                    $argumentCountError = false;
3945 3417
                    $expectedArgumentCountString = null;
3946 3417
                    if (is_numeric($expectedArgumentCount)) {
3947 1684
                        if ($expectedArgumentCount < 0) {
3948 4
                            if ($argumentCount > abs($expectedArgumentCount)) {
3949
                                $argumentCountError = true;
3950 4
                                $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
3951
                            }
3952
                        } else {
3953 1680
                            if ($argumentCount != $expectedArgumentCount) {
3954 78
                                $argumentCountError = true;
3955 1684
                                $expectedArgumentCountString = $expectedArgumentCount;
3956
                            }
3957
                        }
3958 1776
                    } elseif ($expectedArgumentCount != '*') {
3959 1764
                        $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...
3960 1764
                        switch ($argMatch[2]) {
3961 1764
                            case '+':
3962 317
                                if ($argumentCount < $argMatch[1]) {
3963 3
                                    $argumentCountError = true;
3964 3
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
3965
                                }
3966
3967 317
                                break;
3968 1479
                            case '-':
3969 242
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
3970 7
                                    $argumentCountError = true;
3971 7
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
3972
                                }
3973
3974 242
                                break;
3975 1237
                            case ',':
3976 1237
                                if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
3977 24
                                    $argumentCountError = true;
3978 24
                                    $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
3979
                                }
3980
3981 1237
                                break;
3982
                        }
3983
                    }
3984 3417
                    if ($argumentCountError) {
3985 112
                        return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected');
3986
                    }
3987
                }
3988 3306
                ++$index;
3989 3551
            } elseif ($opCharacter == ',') {            //    Is this the separator for function arguments?
3990
                if (
3991 1843
                    !empty($pendingStoreKey) &&
3992 1843
                    $parenthesisDepthMap[$pendingStoreKey] == 0
3993
                ) {
3994
                    // We must go to the IF next argument
3995 34
                    if ($expectingConditionMap[$pendingStoreKey]) {
3996 34
                        $expectingConditionMap[$pendingStoreKey] = false;
3997 34
                        $expectingThenMap[$pendingStoreKey] = true;
3998 31
                    } elseif ($expectingThenMap[$pendingStoreKey]) {
3999 31
                        $expectingThenMap[$pendingStoreKey] = false;
4000 31
                        $expectingElseMap[$pendingStoreKey] = true;
4001
                    } elseif ($expectingElseMap[$pendingStoreKey]) {
4002
                        return $this->raiseFormulaError('Reaching fourth argument of an IF');
4003
                    }
4004
                }
4005 1843
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
4006 271
                    if ($o2 === null) {
4007
                        return $this->raiseFormulaError('Formula Error: Unexpected ,');
4008
                    }
4009 271
                    $output[] = $o2; // pop the argument expression stuff and push onto the output
4010
                }
4011
                //    If we've a comma when we're expecting an operand, then what we actually have is a null operand;
4012
                //        so push a null onto the stack
4013 1843
                if (($expectingOperand) || (!$expectingOperator)) {
4014 25
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
4015
                }
4016
                // make sure there was a function
4017 1843
                $d = $stack->last(2);
4018 1843
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) {
4019
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
4020
                }
4021 1843
                $d = $stack->pop();
4022 1843
                $itemStoreKey = $d['storeKey'] ?? null;
4023 1843
                $itemOnlyIf = $d['onlyIf'] ?? null;
4024 1843
                $itemOnlyIfNot = $d['onlyIfNot'] ?? null;
4025 1843
                $stack->push($d['type'], ++$d['value'], $d['reference'], $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // increment the argument count
4026 1843
                $stack->push('Brace', '(', null, $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // put the ( back on, we'll need to pop back to it again
4027 1843
                $expectingOperator = false;
4028 1843
                $expectingOperand = true;
4029 1843
                ++$index;
4030 3551
            } elseif ($opCharacter == '(' && !$expectingOperator) {
4031 5
                if (!empty($pendingStoreKey)) { // Branch pruning: we go deeper
4032 1
                    ++$parenthesisDepthMap[$pendingStoreKey];
4033
                }
4034 5
                $stack->push('Brace', '(', null, $currentCondition, $currentOnlyIf, $currentOnlyIf);
4035 5
                ++$index;
4036 3551
            } elseif ($isOperandOrFunction && !$expectingOperator) {    // do we now have a function/variable/number?
4037 3551
                $expectingOperator = true;
4038 3551
                $expectingOperand = false;
4039 3551
                $val = $match[1];
4040 3551
                $length = strlen($val);
4041 3551
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) {
4042 3418
                    $val = preg_replace('/\s/u', '', $val);
4043 3418
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
4044 3416
                        $valToUpper = strtoupper($val);
4045
                    } else {
4046 3
                        $valToUpper = 'NAME.ERROR(';
4047
                    }
4048
                    // here $matches[1] will contain values like "IF"
4049
                    // and $val "IF("
4050 3418
                    if ($this->branchPruningEnabled && ($valToUpper == 'IF(')) { // we handle a new if
4051 34
                        $pendingStoreKey = $this->getUnusedBranchStoreKey();
4052 34
                        $pendingStoreKeysStack[] = $pendingStoreKey;
4053 34
                        $expectingConditionMap[$pendingStoreKey] = true;
4054 34
                        $parenthesisDepthMap[$pendingStoreKey] = 0;
4055
                    } else { // this is not an if but we go deeper
4056 3410
                        if (!empty($pendingStoreKey) && array_key_exists($pendingStoreKey, $parenthesisDepthMap)) {
4057 9
                            ++$parenthesisDepthMap[$pendingStoreKey];
4058
                        }
4059
                    }
4060
4061 3418
                    $stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4062
                    // tests if the function is closed right after opening
4063 3418
                    $ax = preg_match('/^\s*\)/u', substr($formula, $index + $length));
4064 3418
                    if ($ax) {
4065 161
                        $stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4066 161
                        $expectingOperator = true;
4067
                    } else {
4068 3326
                        $stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4069 3326
                        $expectingOperator = false;
4070
                    }
4071 3418
                    $stack->push('Brace', '(');
4072 3442
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
4073
                    //    Watch for this case-change when modifying to allow cell references in different worksheets...
4074
                    //    Should only be applied to the actual cell column, not the worksheet name
4075
                    //    If the last entry on the stack was a : operator, then we have a cell range reference
4076 1229
                    $testPrevOp = $stack->last(1);
4077 1229
                    if ($testPrevOp !== null && $testPrevOp['value'] == ':') {
4078
                        //    If we have a worksheet reference, then we're playing with a 3D reference
4079 357
                        if ($matches[2] == '') {
4080
                            //    Otherwise, we 'inherit' the worksheet reference from the start cell reference
4081
                            //    The start of the cell range reference should be the last entry in $output
4082 354
                            $rangeStartCellRef = $output[count($output) - 1]['value'];
4083 354
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches);
4084 354
                            if ($rangeStartMatches[2] > '') {
4085 354
                                $val = $rangeStartMatches[2] . '!' . $val;
4086
                            }
4087
                        } else {
4088 3
                            $rangeStartCellRef = $output[count($output) - 1]['value'];
4089 3
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches);
4090 3
                            if ($rangeStartMatches[2] !== $matches[2]) {
4091 1
                                return $this->raiseFormulaError('3D Range references are not yet supported');
4092
                            }
4093
                        }
4094
                    }
4095
4096 1229
                    $outputItem = $stack->getStackItem('Cell Reference', $val, $val, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4097
4098 1229
                    $output[] = $outputItem;
4099
                } else {    // it's a variable, constant, string, number or boolean
4100
                    //    If the last entry on the stack was a : operator, then we may have a row or column range reference
4101 2493
                    $testPrevOp = $stack->last(1);
4102 2493
                    if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
4103 2
                        $startRowColRef = $output[count($output) - 1]['value'];
4104 2
                        [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
4105 2
                        $rangeSheetRef = $rangeWS1;
4106 2
                        if ($rangeWS1 != '') {
4107
                            $rangeWS1 .= '!';
4108
                        }
4109 2
                        [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
4110 2
                        if ($rangeWS2 != '') {
4111
                            $rangeWS2 .= '!';
4112
                        } else {
4113 2
                            $rangeWS2 = $rangeWS1;
4114
                        }
4115 2
                        $refSheet = $pCellParent;
4116 2
                        if ($pCellParent !== null && $rangeSheetRef !== $pCellParent->getTitle()) {
4117 2
                            $refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef);
4118
                        }
4119
                        if (
4120 2
                            (is_int($startRowColRef)) && (ctype_digit($val)) &&
4121 2
                            ($startRowColRef <= 1048576) && ($val <= 1048576)
4122
                        ) {
4123
                            //    Row range
4124 1
                            $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestColumn() : 'XFD'; //    Max 16,384 columns for Excel2007
4125 1
                            $output[count($output) - 1]['value'] = $rangeWS1 . 'A' . $startRowColRef;
4126 1
                            $val = $rangeWS2 . $endRowColRef . $val;
4127
                        } elseif (
4128 1
                            (ctype_alpha($startRowColRef)) && (ctype_alpha($val)) &&
4129 1
                            (strlen($startRowColRef) <= 3) && (strlen($val) <= 3)
4130
                        ) {
4131
                            //    Column range
4132
                            $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestRow() : 1048576; //    Max 1,048,576 rows for Excel2007
4133
                            $output[count($output) - 1]['value'] = $rangeWS1 . strtoupper($startRowColRef) . '1';
4134
                            $val = $rangeWS2 . $val . $endRowColRef;
4135
                        }
4136
                    }
4137
4138 2493
                    $localeConstant = false;
4139 2493
                    $stackItemType = 'Value';
4140 2493
                    $stackItemReference = null;
4141 2493
                    if ($opCharacter == self::FORMULA_STRING_QUOTE) {
4142
                        //    UnEscape any quotes within the string
4143 941
                        $val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
4144 1934
                    } elseif (is_numeric($val)) {
4145 1695
                        if ((strpos($val, '.') !== false) || (stripos($val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
4146 349
                            $val = (float) $val;
4147
                        } else {
4148 1695
                            $val = (int) $val;
4149
                        }
4150 313
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
4151 228
                        $stackItemType = 'Constant';
4152 228
                        $excelConstant = trim(strtoupper($val));
4153 228
                        $val = self::$excelConstants[$excelConstant];
4154 85
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
4155 18
                        $stackItemType = 'Constant';
4156 18
                        $val = self::$excelConstants[$localeConstant];
4157 67
                    } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', $val, $match)) {
4158 67
                        $stackItemType = 'Defined Name';
4159 67
                        $stackItemReference = $val;
4160
                    }
4161 2493
                    $details = $stack->getStackItem($stackItemType, $val, $stackItemReference, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
4162 2493
                    if ($localeConstant) {
4163 18
                        $details['localeValue'] = $localeConstant;
4164
                    }
4165 2493
                    $output[] = $details;
4166
                }
4167 3551
                $index += $length;
4168 4
            } elseif ($opCharacter == '$') {    // absolute row or column range
4169
                ++$index;
4170 4
            } elseif ($opCharacter == ')') {    // miscellaneous error checking
4171 4
                if ($expectingOperand) {
4172 4
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
4173 4
                    $expectingOperand = false;
4174 4
                    $expectingOperator = true;
4175
                } else {
4176 4
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
4177
                }
4178
            } elseif (isset(self::$operators[$opCharacter]) && !$expectingOperator) {
4179
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
4180
            } else {    // I don't even want to know what you did to get here
4181
                return $this->raiseFormulaError('Formula Error: An unexpected error occurred');
4182
            }
4183
            //    Test for end of formula string
4184 3551
            if ($index == strlen($formula)) {
4185
                //    Did we end with an operator?.
4186
                //    Only valid for the % unary operator
4187 3438
                if ((isset(self::$operators[$opCharacter])) && ($opCharacter != '%')) {
4188
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
4189
                }
4190
4191 3438
                break;
4192
            }
4193
            //    Ignore white space
4194 3545
            while (($formula[$index] == "\n") || ($formula[$index] == "\r")) {
4195
                ++$index;
4196
            }
4197
4198 3545
            if ($formula[$index] == ' ') {
4199 1491
                while ($formula[$index] == ' ') {
4200 1491
                    ++$index;
4201
                }
4202
4203
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
4204
                //        Cell References) then we have an INTERSECTION operator
4205
                if (
4206 1491
                    ($expectingOperator) &&
4207 115
                    ((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
4208 115
                        ($output[count($output) - 1]['type'] == 'Cell Reference') ||
4209 105
                        (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', substr($formula, $index), $match)) &&
4210 1491
                        ($output[count($output) - 1]['type'] == 'Defined Name' || $output[count($output) - 1]['type'] == 'Value')
4211
                    )
4212
                ) {
4213
                    while (
4214 14
                        $stack->count() > 0 &&
4215 14
                        ($o2 = $stack->last()) &&
4216 14
                        isset(self::$operators[$o2['value']]) &&
4217 14
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])
4218
                    ) {
4219 10
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
4220
                    }
4221 14
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
4222 14
                    $expectingOperator = false;
4223
                }
4224
            }
4225
        }
4226
4227 3438
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
4228 210
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
4229
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
4230
            }
4231 210
            $output[] = $op;
4232
        }
4233
4234 3438
        return $output;
4235
    }
4236
4237 582
    private static function dataTestReference(&$operandData)
4238
    {
4239 582
        $operand = $operandData['value'];
4240 582
        if (($operandData['reference'] === null) && (is_array($operand))) {
4241 1
            $rKeys = array_keys($operand);
4242 1
            $rowKey = array_shift($rKeys);
4243 1
            $cKeys = array_keys(array_keys($operand[$rowKey]));
4244 1
            $colKey = array_shift($cKeys);
4245 1
            if (ctype_upper($colKey)) {
4246
                $operandData['reference'] = $colKey . $rowKey;
4247
            }
4248
        }
4249
4250 582
        return $operand;
4251
    }
4252
4253
    // evaluate postfix notation
4254
4255
    /**
4256
     * @param mixed $tokens
4257
     * @param null|string $cellID
4258
     *
4259
     * @return array<int, mixed>|false
4260
     */
4261 3432
    private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null)
4262
    {
4263 3432
        if ($tokens == false) {
4264
            return false;
4265
        }
4266
4267
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
4268
        //        so we store the parent cell collection so that we can re-attach it when necessary
4269 3432
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
4270 3432
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
4271 3432
        $stack = new Stack();
4272
4273
        // Stores branches that have been pruned
4274 3432
        $fakedForBranchPruning = [];
4275
        // help us to know when pruning ['branchTestId' => true/false]
4276 3432
        $branchStore = [];
4277
        //    Loop through each token in turn
4278 3432
        foreach ($tokens as $tokenData) {
4279 3432
            $token = $tokenData['value'];
4280
4281
            // Branch pruning: skip useless resolutions
4282 3432
            $storeKey = $tokenData['storeKey'] ?? null;
4283 3432
            if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) {
4284 29
                $onlyIfStoreKey = $tokenData['onlyIf'];
4285 29
                $storeValue = $branchStore[$onlyIfStoreKey] ?? null;
4286 29
                $storeValueAsBool = ($storeValue === null) ?
4287 29
                    true : (bool) Functions::flattenSingleValue($storeValue);
4288 29
                if (is_array($storeValue)) {
4289 24
                    $wrappedItem = end($storeValue);
4290 24
                    $storeValue = end($wrappedItem);
4291
                }
4292
4293
                if (
4294 29
                    isset($storeValue)
4295
                    && (
4296 28
                        !$storeValueAsBool
4297 28
                        || Functions::isError($storeValue)
4298 29
                        || ($storeValue === 'Pruned branch')
4299
                    )
4300
                ) {
4301
                    // If branching value is not true, we don't need to compute
4302 24
                    if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) {
4303 23
                        $stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token);
4304 23
                        $fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true;
4305
                    }
4306
4307 24
                    if (isset($storeKey)) {
4308
                        // We are processing an if condition
4309
                        // We cascade the pruning to the depending branches
4310 1
                        $branchStore[$storeKey] = 'Pruned branch';
4311 1
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4312 1
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4313
                    }
4314
4315 24
                    continue;
4316
                }
4317
            }
4318
4319 3432
            if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) {
4320 28
                $onlyIfNotStoreKey = $tokenData['onlyIfNot'];
4321 28
                $storeValue = $branchStore[$onlyIfNotStoreKey] ?? null;
4322 28
                $storeValueAsBool = ($storeValue === null) ?
4323 28
                    true : (bool) Functions::flattenSingleValue($storeValue);
4324 28
                if (is_array($storeValue)) {
4325 23
                    $wrappedItem = end($storeValue);
4326 23
                    $storeValue = end($wrappedItem);
4327
                }
4328
                if (
4329 28
                    isset($storeValue)
4330
                    && (
4331 27
                        $storeValueAsBool
4332 27
                        || Functions::isError($storeValue)
4333 28
                        || ($storeValue === 'Pruned branch'))
4334
                ) {
4335
                    // If branching value is true, we don't need to compute
4336 26
                    if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) {
4337 26
                        $stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token);
4338 26
                        $fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true;
4339
                    }
4340
4341 26
                    if (isset($storeKey)) {
4342
                        // We are processing an if condition
4343
                        // We cascade the pruning to the depending branches
4344 2
                        $branchStore[$storeKey] = 'Pruned branch';
4345 2
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4346 2
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4347
                    }
4348
4349 26
                    continue;
4350
                }
4351
            }
4352
4353
            // 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
4354 3432
            if (isset(self::$binaryOperators[$token])) {
4355
                //    We must have two operands, error if we don't
4356 582
                if (($operand2Data = $stack->pop()) === null) {
4357
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4358
                }
4359 582
                if (($operand1Data = $stack->pop()) === null) {
4360
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4361
                }
4362
4363 582
                $operand1 = self::dataTestReference($operand1Data);
4364 582
                $operand2 = self::dataTestReference($operand2Data);
4365
4366
                //    Log what we're doing
4367 582
                if ($token == ':') {
4368 352
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
4369
                } else {
4370 313
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
4371
                }
4372
4373
                //    Process the operation in the appropriate manner
4374
                switch ($token) {
4375
                    //    Comparison (Boolean) Operators
4376 582
                    case '>':            //    Greater than
4377 567
                    case '<':            //    Less than
4378 554
                    case '>=':            //    Greater than or Equal to
4379 546
                    case '<=':            //    Less than or Equal to
4380 536
                    case '=':            //    Equality
4381 454
                    case '<>':            //    Inequality
4382 183
                        $result = $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
4383 183
                        if (isset($storeKey)) {
4384 25
                            $branchStore[$storeKey] = $result;
4385
                        }
4386
4387 183
                        break;
4388
                    //    Binary Operators
4389 443
                    case ':':            //    Range
4390 352
                        if (strpos($operand1Data['reference'], '!') !== false) {
4391 347
                            [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
4392
                        } else {
4393 10
                            $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

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

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

4624
                            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...
4625 1056
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4626 1056
                                $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

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

5178
            $namedRange = DefinedName::resolveName($pRange, /** @scrutinizer ignore-type */ $pSheet);
Loading history...
5179
            if ($namedRange === null) {
5180
                return Functions::REF();
5181
            }
5182
5183
            $pSheet = $namedRange->getWorksheet();
5184
            $pRange = $namedRange->getValue();
5185
            $splitRange = Coordinate::splitRange($pRange);
5186
            //    Convert row and column references
5187
            if (ctype_alpha($splitRange[0][0])) {
5188
                $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
5189
            } elseif (ctype_digit($splitRange[0][0])) {
5190
                $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
5191
            }
5192
5193
            // Extract range
5194
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
5195
            if (!isset($aReferences[1])) {
5196
                //    Single cell (or single column or row) in range
5197
                [$currentCol, $currentRow] = Coordinate::coordinateFromString($aReferences[0]);
5198
                if ($pSheet->cellExists($aReferences[0])) {
5199
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5200
                } else {
5201
                    $returnValue[$currentRow][$currentCol] = null;
5202
                }
5203
            } else {
5204
                // Extract cell data for all cells in the range
5205
                foreach ($aReferences as $reference) {
5206
                    // Extract range
5207
                    [$currentCol, $currentRow] = Coordinate::coordinateFromString($reference);
5208
                    if ($pSheet->cellExists($reference)) {
5209
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
5210
                    } else {
5211
                        $returnValue[$currentRow][$currentCol] = null;
5212
                    }
5213
                }
5214
            }
5215
        }
5216
5217
        return $returnValue;
5218
    }
5219
5220
    /**
5221
     * Is a specific function implemented?
5222
     *
5223
     * @param string $pFunction Function Name
5224
     *
5225
     * @return bool
5226
     */
5227 3
    public function isImplemented($pFunction)
5228
    {
5229 3
        $pFunction = strtoupper($pFunction);
5230 3
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
5231
5232 3
        return !$notImplemented;
5233
    }
5234
5235
    /**
5236
     * Get a list of all implemented functions as an array of function objects.
5237
     */
5238
    public function getFunctions(): array
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 3299
    private function addCellReference(array $args, $passCellReference, $functionCall, ?Cell $pCell = null)
5269
    {
5270 3299
        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 3299
        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->getCoordinate();
5338
5339
        // Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns
5340 64
        $definedNameValue = self::$referenceHelper->updateFormulaReferencesAnyWorksheet(
5341
            $definedNameValue,
5342 64
            Coordinate::columnIndexFromString($pCell->getColumn()) - 1,
5343 64
            $pCell->getRow() - 1
5344
        );
5345
5346 64
        $this->debugLog->writeDebugLog("Value adjusted for relative references is {$definedNameValue}");
5347
5348 64
        $recursiveCalculator = new self($this->spreadsheet);
5349 64
        $recursiveCalculator->getDebugLog()->setWriteDebugLog($this->getDebugLog()->getWriteDebugLog());
5350 64
        $recursiveCalculator->getDebugLog()->setEchoDebugLog($this->getDebugLog()->getEchoDebugLog());
5351 64
        $result = $recursiveCalculator->_calculateFormulaValue($definedNameValue, $recursiveCalculationCellAddress, $recursiveCalculationCell);
5352
5353 64
        if ($this->getDebugLog()->getWriteDebugLog()) {
5354
            $this->debugLog->mergeDebugLog(array_slice($recursiveCalculator->getDebugLog()->getLog(), 3));
5355
            $this->debugLog->writeDebugLog("Evaluation Result for Named {$definedNameType} {$namedRange->getName()} is {$this->showTypeDetails($result)}");
5356
        }
5357
5358 64
        $stack->push('Defined Name', $result, $namedRange->getName());
5359
5360 64
        return $result;
5361
    }
5362
}
5363