Passed
Pull Request — master (#4143)
by Owen
14:43
created

Calculation::validateBinaryOperand()   B

Complexity

Conditions 11
Paths 27

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 11.0245

Importance

Changes 0
Metric Value
cc 11
eloc 18
c 0
b 0
f 0
nc 27
nop 2
dl 0
loc 36
rs 7.3166
ccs 16
cts 17
cp 0.9412
crap 11.0245

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Engine\BranchPruner;
6
use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack;
7
use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger;
8
use PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands;
9
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
10
use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack;
11
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
12
use PhpOffice\PhpSpreadsheet\Cell\Cell;
13
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
14
use PhpOffice\PhpSpreadsheet\Cell\DataType;
15
use PhpOffice\PhpSpreadsheet\DefinedName;
16
use PhpOffice\PhpSpreadsheet\NamedRange;
17
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
18
use PhpOffice\PhpSpreadsheet\Shared;
19
use PhpOffice\PhpSpreadsheet\Spreadsheet;
20
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
21
use ReflectionClassConstant;
22
use ReflectionMethod;
23
use ReflectionParameter;
24
use Throwable;
25
26
class Calculation
27
{
28
    /** Constants                */
29
    /** Regular Expressions        */
30
    //    Numeric operand
31
    const CALCULATION_REGEXP_NUMBER = '[-+]?\d*\.?\d+(e[-+]?\d+)?';
32
    //    String operand
33
    const CALCULATION_REGEXP_STRING = '"(?:[^"]|"")*"';
34
    //    Opening bracket
35
    const CALCULATION_REGEXP_OPENBRACE = '\(';
36
    //    Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
37
    const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
38
    //    Strip xlfn and xlws prefixes from function name
39
    const CALCULATION_REGEXP_STRIP_XLFN_XLWS = '/(_xlfn[.])?(_xlws[.])?(?=[\p{L}][\p{L}\p{N}\.]*[\s]*[(])/';
40
    //    Cell reference (cell or range of cells, with or without a sheet reference)
41
    const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
42
    // Used only to detect spill operator #
43
    const CALCULATION_REGEXP_CELLREF_SPILL = '/' . self::CALCULATION_REGEXP_CELLREF . '#/i';
44
    //    Cell reference (with or without a sheet reference) ensuring absolute/relative
45
    const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])';
46
    const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\".(?:[^\"]|\"[^!])?\"))!)?(\$?[a-z]{1,3})):(?![.*])';
47
    const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])';
48
    //    Cell reference (with or without a sheet reference) ensuring absolute/relative
49
    //    Cell ranges ensuring absolute/relative
50
    const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
51
    const CALCULATION_REGEXP_ROWRANGE_RELATIVE = '(\$?\d{1,7}):(\$?\d{1,7})';
52
    //    Defined Names: Named Range of cells, or Named Formulae
53
    const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)';
54
    // Structured Reference (Fully Qualified and Unqualified)
55
    const CALCULATION_REGEXP_STRUCTURED_REFERENCE = '([\p{L}_\\\\][\p{L}\p{N}\._]+)?(\[(?:[^\d\]+-])?)';
56
    //    Error
57
    const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?';
58
59
    /** constants */
60
    const RETURN_ARRAY_AS_ERROR = 'error';
61
    const RETURN_ARRAY_AS_VALUE = 'value';
62
    const RETURN_ARRAY_AS_ARRAY = 'array';
63
64
    const FORMULA_OPEN_FUNCTION_BRACE = '(';
65
    const FORMULA_CLOSE_FUNCTION_BRACE = ')';
66
    const FORMULA_OPEN_MATRIX_BRACE = '{';
67
    const FORMULA_CLOSE_MATRIX_BRACE = '}';
68
    const FORMULA_STRING_QUOTE = '"';
69
70
    /** Preferable to use instance variable instanceArrayReturnType rather than this static property. */
71
    private static string $returnArrayAsType = self::RETURN_ARRAY_AS_VALUE;
72
73
    /** Preferable to use this instance variable rather than static returnArrayAsType */
74
    private ?string $instanceArrayReturnType = null;
75
76
    /**
77
     * Instance of this class.
78
     */
79
    private static ?Calculation $instance = null;
80
81
    /**
82
     * Instance of the spreadsheet this Calculation Engine is using.
83
     */
84
    private ?Spreadsheet $spreadsheet;
85
86
    /**
87
     * Calculation cache.
88
     */
89
    private array $calculationCache = [];
90
91
    /**
92
     * Calculation cache enabled.
93
     */
94
    private bool $calculationCacheEnabled = true;
95
96
    private BranchPruner $branchPruner;
97
98
    private bool $branchPruningEnabled = true;
99
100
    /**
101
     * List of operators that can be used within formulae
102
     * The true/false value indicates whether it is a binary operator or a unary operator.
103
     */
104
    private const CALCULATION_OPERATORS = [
105
        '+' => true, '-' => true, '*' => true, '/' => true,
106
        '^' => true, '&' => true, '%' => false, '~' => false,
107
        '>' => true, '<' => true, '=' => true, '>=' => true,
108
        '<=' => true, '<>' => true, '∩' => true, '∪' => true,
109
        ':' => true,
110
    ];
111
112
    /**
113
     * List of binary operators (those that expect two operands).
114
     */
115
    private const BINARY_OPERATORS = [
116
        '+' => true, '-' => true, '*' => true, '/' => true,
117
        '^' => true, '&' => true, '>' => true, '<' => true,
118
        '=' => true, '>=' => true, '<=' => true, '<>' => true,
119
        '∩' => true, '∪' => true, ':' => true,
120
    ];
121
122
    /**
123
     * The debug log generated by the calculation engine.
124
     */
125
    private Logger $debugLog;
126
127
    private bool $suppressFormulaErrors = false;
128
129
    private bool $processingAnchorArray = false;
130
131
    /**
132
     * Error message for any error that was raised/thrown by the calculation engine.
133
     */
134
    public ?string $formulaError = null;
135
136
    /**
137
     * Reference Helper.
138
     */
139
    private static ReferenceHelper $referenceHelper;
140
141
    /**
142
     * An array of the nested cell references accessed by the calculation engine, used for the debug log.
143
     */
144
    private CyclicReferenceStack $cyclicReferenceStack;
145
146
    private array $cellStack = [];
147
148
    /**
149
     * Current iteration counter for cyclic formulae
150
     * If the value is 0 (or less) then cyclic formulae will throw an exception,
151
     * otherwise they will iterate to the limit defined here before returning a result.
152
     */
153
    private int $cyclicFormulaCounter = 1;
154
155
    private string $cyclicFormulaCell = '';
156
157
    /**
158
     * Number of iterations for cyclic formulae.
159
     */
160
    public int $cyclicFormulaCount = 1;
161
162
    /**
163
     * The current locale setting.
164
     */
165
    private static string $localeLanguage = 'en_us'; //    US English    (default locale)
166
167
    /**
168
     * List of available locale settings
169
     * Note that this is read for the locale subdirectory only when requested.
170
     *
171
     * @var string[]
172
     */
173
    private static array $validLocaleLanguages = [
174
        'en', //    English        (default language)
175
    ];
176
177
    /**
178
     * Locale-specific argument separator for function arguments.
179
     */
180
    private static string $localeArgumentSeparator = ',';
181
182
    private static array $localeFunctions = [];
183
184
    /**
185
     * Locale-specific translations for Excel constants (True, False and Null).
186
     *
187
     * @var array<string, string>
188
     */
189
    private static array $localeBoolean = [
190
        'TRUE' => 'TRUE',
191
        'FALSE' => 'FALSE',
192
        'NULL' => 'NULL',
193
    ];
194
195 7
    public static function getLocaleBoolean(string $index): string
196
    {
197 7
        return self::$localeBoolean[$index];
198
    }
199
200
    /**
201
     * Excel constant string translations to their PHP equivalents
202
     * Constant conversion from text name/value to actual (datatyped) value.
203
     *
204
     * @var array<string, null|bool>
205
     */
206
    private static array $excelConstants = [
207
        'TRUE' => true,
208
        'FALSE' => false,
209
        'NULL' => null,
210
    ];
211
212 20
    public static function keyInExcelConstants(string $key): bool
213
    {
214 20
        return array_key_exists($key, self::$excelConstants);
215
    }
216
217 3
    public static function getExcelConstants(string $key): bool|null
218
    {
219 3
        return self::$excelConstants[$key];
220
    }
221
222
    /**
223
     * Array of functions usable on Spreadsheet.
224
     * In theory, this could be const rather than static;
225
     *   however, Phpstan breaks trying to analyze it when attempted.
226
     */
227
    private static array $phpSpreadsheetFunctions = [
228
        'ABS' => [
229
            'category' => Category::CATEGORY_MATH_AND_TRIG,
230
            'functionCall' => [MathTrig\Absolute::class, 'evaluate'],
231
            'argumentCount' => '1',
232
        ],
233
        'ACCRINT' => [
234
            'category' => Category::CATEGORY_FINANCIAL,
235
            'functionCall' => [Financial\Securities\AccruedInterest::class, 'periodic'],
236
            'argumentCount' => '4-8',
237
        ],
238
        'ACCRINTM' => [
239
            'category' => Category::CATEGORY_FINANCIAL,
240
            'functionCall' => [Financial\Securities\AccruedInterest::class, 'atMaturity'],
241
            'argumentCount' => '3-5',
242
        ],
243
        'ACOS' => [
244
            'category' => Category::CATEGORY_MATH_AND_TRIG,
245
            'functionCall' => [MathTrig\Trig\Cosine::class, 'acos'],
246
            'argumentCount' => '1',
247
        ],
248
        'ACOSH' => [
249
            'category' => Category::CATEGORY_MATH_AND_TRIG,
250
            'functionCall' => [MathTrig\Trig\Cosine::class, 'acosh'],
251
            'argumentCount' => '1',
252
        ],
253
        'ACOT' => [
254
            'category' => Category::CATEGORY_MATH_AND_TRIG,
255
            'functionCall' => [MathTrig\Trig\Cotangent::class, 'acot'],
256
            'argumentCount' => '1',
257
        ],
258
        'ACOTH' => [
259
            'category' => Category::CATEGORY_MATH_AND_TRIG,
260
            'functionCall' => [MathTrig\Trig\Cotangent::class, 'acoth'],
261
            'argumentCount' => '1',
262
        ],
263
        'ADDRESS' => [
264
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
265
            'functionCall' => [LookupRef\Address::class, 'cell'],
266
            'argumentCount' => '2-5',
267
        ],
268
        'AGGREGATE' => [
269
            'category' => Category::CATEGORY_MATH_AND_TRIG,
270
            'functionCall' => [Functions::class, 'DUMMY'],
271
            'argumentCount' => '3+',
272
        ],
273
        'AMORDEGRC' => [
274
            'category' => Category::CATEGORY_FINANCIAL,
275
            'functionCall' => [Financial\Amortization::class, 'AMORDEGRC'],
276
            'argumentCount' => '6,7',
277
        ],
278
        'AMORLINC' => [
279
            'category' => Category::CATEGORY_FINANCIAL,
280
            'functionCall' => [Financial\Amortization::class, 'AMORLINC'],
281
            'argumentCount' => '6,7',
282
        ],
283
        'ANCHORARRAY' => [
284
            'category' => Category::CATEGORY_MICROSOFT_INTERNAL,
285
            'functionCall' => [Internal\ExcelArrayPseudoFunctions::class, 'anchorArray'],
286
            'argumentCount' => '1',
287
            'passCellReference' => true,
288
            'passByReference' => [true],
289
        ],
290
        'AND' => [
291
            'category' => Category::CATEGORY_LOGICAL,
292
            'functionCall' => [Logical\Operations::class, 'logicalAnd'],
293
            'argumentCount' => '1+',
294
        ],
295
        'ARABIC' => [
296
            'category' => Category::CATEGORY_MATH_AND_TRIG,
297
            'functionCall' => [MathTrig\Arabic::class, 'evaluate'],
298
            'argumentCount' => '1',
299
        ],
300
        'AREAS' => [
301
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
302
            'functionCall' => [Functions::class, 'DUMMY'],
303
            'argumentCount' => '1',
304
        ],
305
        'ARRAYTOTEXT' => [
306
            'category' => Category::CATEGORY_TEXT_AND_DATA,
307
            'functionCall' => [TextData\Text::class, 'fromArray'],
308
            'argumentCount' => '1,2',
309
        ],
310
        'ASC' => [
311
            'category' => Category::CATEGORY_TEXT_AND_DATA,
312
            'functionCall' => [Functions::class, 'DUMMY'],
313
            'argumentCount' => '1',
314
        ],
315
        'ASIN' => [
316
            'category' => Category::CATEGORY_MATH_AND_TRIG,
317
            'functionCall' => [MathTrig\Trig\Sine::class, 'asin'],
318
            'argumentCount' => '1',
319
        ],
320
        'ASINH' => [
321
            'category' => Category::CATEGORY_MATH_AND_TRIG,
322
            'functionCall' => [MathTrig\Trig\Sine::class, 'asinh'],
323
            'argumentCount' => '1',
324
        ],
325
        'ATAN' => [
326
            'category' => Category::CATEGORY_MATH_AND_TRIG,
327
            'functionCall' => [MathTrig\Trig\Tangent::class, 'atan'],
328
            'argumentCount' => '1',
329
        ],
330
        'ATAN2' => [
331
            'category' => Category::CATEGORY_MATH_AND_TRIG,
332
            'functionCall' => [MathTrig\Trig\Tangent::class, 'atan2'],
333
            'argumentCount' => '2',
334
        ],
335
        'ATANH' => [
336
            'category' => Category::CATEGORY_MATH_AND_TRIG,
337
            'functionCall' => [MathTrig\Trig\Tangent::class, 'atanh'],
338
            'argumentCount' => '1',
339
        ],
340
        'AVEDEV' => [
341
            'category' => Category::CATEGORY_STATISTICAL,
342
            'functionCall' => [Statistical\Averages::class, 'averageDeviations'],
343
            'argumentCount' => '1+',
344
        ],
345
        'AVERAGE' => [
346
            'category' => Category::CATEGORY_STATISTICAL,
347
            'functionCall' => [Statistical\Averages::class, 'average'],
348
            'argumentCount' => '1+',
349
        ],
350
        'AVERAGEA' => [
351
            'category' => Category::CATEGORY_STATISTICAL,
352
            'functionCall' => [Statistical\Averages::class, 'averageA'],
353
            'argumentCount' => '1+',
354
        ],
355
        'AVERAGEIF' => [
356
            'category' => Category::CATEGORY_STATISTICAL,
357
            'functionCall' => [Statistical\Conditional::class, 'AVERAGEIF'],
358
            'argumentCount' => '2,3',
359
        ],
360
        'AVERAGEIFS' => [
361
            'category' => Category::CATEGORY_STATISTICAL,
362
            'functionCall' => [Statistical\Conditional::class, 'AVERAGEIFS'],
363
            'argumentCount' => '3+',
364
        ],
365
        'BAHTTEXT' => [
366
            'category' => Category::CATEGORY_TEXT_AND_DATA,
367
            'functionCall' => [Functions::class, 'DUMMY'],
368
            'argumentCount' => '1',
369
        ],
370
        'BASE' => [
371
            'category' => Category::CATEGORY_MATH_AND_TRIG,
372
            'functionCall' => [MathTrig\Base::class, 'evaluate'],
373
            'argumentCount' => '2,3',
374
        ],
375
        'BESSELI' => [
376
            'category' => Category::CATEGORY_ENGINEERING,
377
            'functionCall' => [Engineering\BesselI::class, 'BESSELI'],
378
            'argumentCount' => '2',
379
        ],
380
        'BESSELJ' => [
381
            'category' => Category::CATEGORY_ENGINEERING,
382
            'functionCall' => [Engineering\BesselJ::class, 'BESSELJ'],
383
            'argumentCount' => '2',
384
        ],
385
        'BESSELK' => [
386
            'category' => Category::CATEGORY_ENGINEERING,
387
            'functionCall' => [Engineering\BesselK::class, 'BESSELK'],
388
            'argumentCount' => '2',
389
        ],
390
        'BESSELY' => [
391
            'category' => Category::CATEGORY_ENGINEERING,
392
            'functionCall' => [Engineering\BesselY::class, 'BESSELY'],
393
            'argumentCount' => '2',
394
        ],
395
        'BETADIST' => [
396
            'category' => Category::CATEGORY_STATISTICAL,
397
            'functionCall' => [Statistical\Distributions\Beta::class, 'distribution'],
398
            'argumentCount' => '3-5',
399
        ],
400
        'BETA.DIST' => [
401
            'category' => Category::CATEGORY_STATISTICAL,
402
            'functionCall' => [Functions::class, 'DUMMY'],
403
            'argumentCount' => '4-6',
404
        ],
405
        'BETAINV' => [
406
            'category' => Category::CATEGORY_STATISTICAL,
407
            'functionCall' => [Statistical\Distributions\Beta::class, 'inverse'],
408
            'argumentCount' => '3-5',
409
        ],
410
        'BETA.INV' => [
411
            'category' => Category::CATEGORY_STATISTICAL,
412
            'functionCall' => [Statistical\Distributions\Beta::class, 'inverse'],
413
            'argumentCount' => '3-5',
414
        ],
415
        'BIN2DEC' => [
416
            'category' => Category::CATEGORY_ENGINEERING,
417
            'functionCall' => [Engineering\ConvertBinary::class, 'toDecimal'],
418
            'argumentCount' => '1',
419
        ],
420
        'BIN2HEX' => [
421
            'category' => Category::CATEGORY_ENGINEERING,
422
            'functionCall' => [Engineering\ConvertBinary::class, 'toHex'],
423
            'argumentCount' => '1,2',
424
        ],
425
        'BIN2OCT' => [
426
            'category' => Category::CATEGORY_ENGINEERING,
427
            'functionCall' => [Engineering\ConvertBinary::class, 'toOctal'],
428
            'argumentCount' => '1,2',
429
        ],
430
        'BINOMDIST' => [
431
            'category' => Category::CATEGORY_STATISTICAL,
432
            'functionCall' => [Statistical\Distributions\Binomial::class, 'distribution'],
433
            'argumentCount' => '4',
434
        ],
435
        'BINOM.DIST' => [
436
            'category' => Category::CATEGORY_STATISTICAL,
437
            'functionCall' => [Statistical\Distributions\Binomial::class, 'distribution'],
438
            'argumentCount' => '4',
439
        ],
440
        'BINOM.DIST.RANGE' => [
441
            'category' => Category::CATEGORY_STATISTICAL,
442
            'functionCall' => [Statistical\Distributions\Binomial::class, 'range'],
443
            'argumentCount' => '3,4',
444
        ],
445
        'BINOM.INV' => [
446
            'category' => Category::CATEGORY_STATISTICAL,
447
            'functionCall' => [Statistical\Distributions\Binomial::class, 'inverse'],
448
            'argumentCount' => '3',
449
        ],
450
        'BITAND' => [
451
            'category' => Category::CATEGORY_ENGINEERING,
452
            'functionCall' => [Engineering\BitWise::class, 'BITAND'],
453
            'argumentCount' => '2',
454
        ],
455
        'BITOR' => [
456
            'category' => Category::CATEGORY_ENGINEERING,
457
            'functionCall' => [Engineering\BitWise::class, 'BITOR'],
458
            'argumentCount' => '2',
459
        ],
460
        'BITXOR' => [
461
            'category' => Category::CATEGORY_ENGINEERING,
462
            'functionCall' => [Engineering\BitWise::class, 'BITXOR'],
463
            'argumentCount' => '2',
464
        ],
465
        'BITLSHIFT' => [
466
            'category' => Category::CATEGORY_ENGINEERING,
467
            'functionCall' => [Engineering\BitWise::class, 'BITLSHIFT'],
468
            'argumentCount' => '2',
469
        ],
470
        'BITRSHIFT' => [
471
            'category' => Category::CATEGORY_ENGINEERING,
472
            'functionCall' => [Engineering\BitWise::class, 'BITRSHIFT'],
473
            'argumentCount' => '2',
474
        ],
475
        'BYCOL' => [
476
            'category' => Category::CATEGORY_LOGICAL,
477
            'functionCall' => [Functions::class, 'DUMMY'],
478
            'argumentCount' => '*',
479
        ],
480
        'BYROW' => [
481
            'category' => Category::CATEGORY_LOGICAL,
482
            'functionCall' => [Functions::class, 'DUMMY'],
483
            'argumentCount' => '*',
484
        ],
485
        'CEILING' => [
486
            'category' => Category::CATEGORY_MATH_AND_TRIG,
487
            'functionCall' => [MathTrig\Ceiling::class, 'ceiling'],
488
            'argumentCount' => '1-2', // 2 for Excel, 1-2 for Ods/Gnumeric
489
        ],
490
        'CEILING.MATH' => [
491
            'category' => Category::CATEGORY_MATH_AND_TRIG,
492
            'functionCall' => [MathTrig\Ceiling::class, 'math'],
493
            'argumentCount' => '1-3',
494
        ],
495
        'CEILING.PRECISE' => [
496
            'category' => Category::CATEGORY_MATH_AND_TRIG,
497
            'functionCall' => [MathTrig\Ceiling::class, 'precise'],
498
            'argumentCount' => '1,2',
499
        ],
500
        'CELL' => [
501
            'category' => Category::CATEGORY_INFORMATION,
502
            'functionCall' => [Functions::class, 'DUMMY'],
503
            'argumentCount' => '1,2',
504
        ],
505
        'CHAR' => [
506
            'category' => Category::CATEGORY_TEXT_AND_DATA,
507
            'functionCall' => [TextData\CharacterConvert::class, 'character'],
508
            'argumentCount' => '1',
509
        ],
510
        'CHIDIST' => [
511
            'category' => Category::CATEGORY_STATISTICAL,
512
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionRightTail'],
513
            'argumentCount' => '2',
514
        ],
515
        'CHISQ.DIST' => [
516
            'category' => Category::CATEGORY_STATISTICAL,
517
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionLeftTail'],
518
            'argumentCount' => '3',
519
        ],
520
        'CHISQ.DIST.RT' => [
521
            'category' => Category::CATEGORY_STATISTICAL,
522
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionRightTail'],
523
            'argumentCount' => '2',
524
        ],
525
        'CHIINV' => [
526
            'category' => Category::CATEGORY_STATISTICAL,
527
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'],
528
            'argumentCount' => '2',
529
        ],
530
        'CHISQ.INV' => [
531
            'category' => Category::CATEGORY_STATISTICAL,
532
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseLeftTail'],
533
            'argumentCount' => '2',
534
        ],
535
        'CHISQ.INV.RT' => [
536
            'category' => Category::CATEGORY_STATISTICAL,
537
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'],
538
            'argumentCount' => '2',
539
        ],
540
        'CHITEST' => [
541
            'category' => Category::CATEGORY_STATISTICAL,
542
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'test'],
543
            'argumentCount' => '2',
544
        ],
545
        'CHISQ.TEST' => [
546
            'category' => Category::CATEGORY_STATISTICAL,
547
            'functionCall' => [Statistical\Distributions\ChiSquared::class, 'test'],
548
            'argumentCount' => '2',
549
        ],
550
        'CHOOSE' => [
551
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
552
            'functionCall' => [LookupRef\Selection::class, 'CHOOSE'],
553
            'argumentCount' => '2+',
554
        ],
555
        'CHOOSECOLS' => [
556
            'category' => Category::CATEGORY_MATH_AND_TRIG,
557
            'functionCall' => [Functions::class, 'DUMMY'],
558
            'argumentCount' => '2+',
559
        ],
560
        'CHOOSEROWS' => [
561
            'category' => Category::CATEGORY_MATH_AND_TRIG,
562
            'functionCall' => [Functions::class, 'DUMMY'],
563
            'argumentCount' => '2+',
564
        ],
565
        'CLEAN' => [
566
            'category' => Category::CATEGORY_TEXT_AND_DATA,
567
            'functionCall' => [TextData\Trim::class, 'nonPrintable'],
568
            'argumentCount' => '1',
569
        ],
570
        'CODE' => [
571
            'category' => Category::CATEGORY_TEXT_AND_DATA,
572
            'functionCall' => [TextData\CharacterConvert::class, 'code'],
573
            'argumentCount' => '1',
574
        ],
575
        'COLUMN' => [
576
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
577
            'functionCall' => [LookupRef\RowColumnInformation::class, 'COLUMN'],
578
            'argumentCount' => '-1',
579
            'passCellReference' => true,
580
            'passByReference' => [true],
581
        ],
582
        'COLUMNS' => [
583
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
584
            'functionCall' => [LookupRef\RowColumnInformation::class, 'COLUMNS'],
585
            'argumentCount' => '1',
586
        ],
587
        'COMBIN' => [
588
            'category' => Category::CATEGORY_MATH_AND_TRIG,
589
            'functionCall' => [MathTrig\Combinations::class, 'withoutRepetition'],
590
            'argumentCount' => '2',
591
        ],
592
        'COMBINA' => [
593
            'category' => Category::CATEGORY_MATH_AND_TRIG,
594
            'functionCall' => [MathTrig\Combinations::class, 'withRepetition'],
595
            'argumentCount' => '2',
596
        ],
597
        'COMPLEX' => [
598
            'category' => Category::CATEGORY_ENGINEERING,
599
            'functionCall' => [Engineering\Complex::class, 'COMPLEX'],
600
            'argumentCount' => '2,3',
601
        ],
602
        'CONCAT' => [
603
            'category' => Category::CATEGORY_TEXT_AND_DATA,
604
            'functionCall' => [TextData\Concatenate::class, 'CONCATENATE'],
605
            'argumentCount' => '1+',
606
        ],
607
        'CONCATENATE' => [
608
            'category' => Category::CATEGORY_TEXT_AND_DATA,
609
            'functionCall' => [TextData\Concatenate::class, 'actualCONCATENATE'],
610
            'argumentCount' => '1+',
611
        ],
612
        'CONFIDENCE' => [
613
            'category' => Category::CATEGORY_STATISTICAL,
614
            'functionCall' => [Statistical\Confidence::class, 'CONFIDENCE'],
615
            'argumentCount' => '3',
616
        ],
617
        'CONFIDENCE.NORM' => [
618
            'category' => Category::CATEGORY_STATISTICAL,
619
            'functionCall' => [Statistical\Confidence::class, 'CONFIDENCE'],
620
            'argumentCount' => '3',
621
        ],
622
        'CONFIDENCE.T' => [
623
            'category' => Category::CATEGORY_STATISTICAL,
624
            'functionCall' => [Functions::class, 'DUMMY'],
625
            'argumentCount' => '3',
626
        ],
627
        'CONVERT' => [
628
            'category' => Category::CATEGORY_ENGINEERING,
629
            'functionCall' => [Engineering\ConvertUOM::class, 'CONVERT'],
630
            'argumentCount' => '3',
631
        ],
632
        'CORREL' => [
633
            'category' => Category::CATEGORY_STATISTICAL,
634
            'functionCall' => [Statistical\Trends::class, 'CORREL'],
635
            'argumentCount' => '2',
636
        ],
637
        'COS' => [
638
            'category' => Category::CATEGORY_MATH_AND_TRIG,
639
            'functionCall' => [MathTrig\Trig\Cosine::class, 'cos'],
640
            'argumentCount' => '1',
641
        ],
642
        'COSH' => [
643
            'category' => Category::CATEGORY_MATH_AND_TRIG,
644
            'functionCall' => [MathTrig\Trig\Cosine::class, 'cosh'],
645
            'argumentCount' => '1',
646
        ],
647
        'COT' => [
648
            'category' => Category::CATEGORY_MATH_AND_TRIG,
649
            'functionCall' => [MathTrig\Trig\Cotangent::class, 'cot'],
650
            'argumentCount' => '1',
651
        ],
652
        'COTH' => [
653
            'category' => Category::CATEGORY_MATH_AND_TRIG,
654
            'functionCall' => [MathTrig\Trig\Cotangent::class, 'coth'],
655
            'argumentCount' => '1',
656
        ],
657
        'COUNT' => [
658
            'category' => Category::CATEGORY_STATISTICAL,
659
            'functionCall' => [Statistical\Counts::class, 'COUNT'],
660
            'argumentCount' => '1+',
661
        ],
662
        'COUNTA' => [
663
            'category' => Category::CATEGORY_STATISTICAL,
664
            'functionCall' => [Statistical\Counts::class, 'COUNTA'],
665
            'argumentCount' => '1+',
666
        ],
667
        'COUNTBLANK' => [
668
            'category' => Category::CATEGORY_STATISTICAL,
669
            'functionCall' => [Statistical\Counts::class, 'COUNTBLANK'],
670
            'argumentCount' => '1',
671
        ],
672
        'COUNTIF' => [
673
            'category' => Category::CATEGORY_STATISTICAL,
674
            'functionCall' => [Statistical\Conditional::class, 'COUNTIF'],
675
            'argumentCount' => '2',
676
        ],
677
        'COUNTIFS' => [
678
            'category' => Category::CATEGORY_STATISTICAL,
679
            'functionCall' => [Statistical\Conditional::class, 'COUNTIFS'],
680
            'argumentCount' => '2+',
681
        ],
682
        'COUPDAYBS' => [
683
            'category' => Category::CATEGORY_FINANCIAL,
684
            'functionCall' => [Financial\Coupons::class, 'COUPDAYBS'],
685
            'argumentCount' => '3,4',
686
        ],
687
        'COUPDAYS' => [
688
            'category' => Category::CATEGORY_FINANCIAL,
689
            'functionCall' => [Financial\Coupons::class, 'COUPDAYS'],
690
            'argumentCount' => '3,4',
691
        ],
692
        'COUPDAYSNC' => [
693
            'category' => Category::CATEGORY_FINANCIAL,
694
            'functionCall' => [Financial\Coupons::class, 'COUPDAYSNC'],
695
            'argumentCount' => '3,4',
696
        ],
697
        'COUPNCD' => [
698
            'category' => Category::CATEGORY_FINANCIAL,
699
            'functionCall' => [Financial\Coupons::class, 'COUPNCD'],
700
            'argumentCount' => '3,4',
701
        ],
702
        'COUPNUM' => [
703
            'category' => Category::CATEGORY_FINANCIAL,
704
            'functionCall' => [Financial\Coupons::class, 'COUPNUM'],
705
            'argumentCount' => '3,4',
706
        ],
707
        'COUPPCD' => [
708
            'category' => Category::CATEGORY_FINANCIAL,
709
            'functionCall' => [Financial\Coupons::class, 'COUPPCD'],
710
            'argumentCount' => '3,4',
711
        ],
712
        'COVAR' => [
713
            'category' => Category::CATEGORY_STATISTICAL,
714
            'functionCall' => [Statistical\Trends::class, 'COVAR'],
715
            'argumentCount' => '2',
716
        ],
717
        'COVARIANCE.P' => [
718
            'category' => Category::CATEGORY_STATISTICAL,
719
            'functionCall' => [Statistical\Trends::class, 'COVAR'],
720
            'argumentCount' => '2',
721
        ],
722
        'COVARIANCE.S' => [
723
            'category' => Category::CATEGORY_STATISTICAL,
724
            'functionCall' => [Functions::class, 'DUMMY'],
725
            'argumentCount' => '2',
726
        ],
727
        'CRITBINOM' => [
728
            'category' => Category::CATEGORY_STATISTICAL,
729
            'functionCall' => [Statistical\Distributions\Binomial::class, 'inverse'],
730
            'argumentCount' => '3',
731
        ],
732
        'CSC' => [
733
            'category' => Category::CATEGORY_MATH_AND_TRIG,
734
            'functionCall' => [MathTrig\Trig\Cosecant::class, 'csc'],
735
            'argumentCount' => '1',
736
        ],
737
        'CSCH' => [
738
            'category' => Category::CATEGORY_MATH_AND_TRIG,
739
            'functionCall' => [MathTrig\Trig\Cosecant::class, 'csch'],
740
            'argumentCount' => '1',
741
        ],
742
        'CUBEKPIMEMBER' => [
743
            'category' => Category::CATEGORY_CUBE,
744
            'functionCall' => [Functions::class, 'DUMMY'],
745
            'argumentCount' => '?',
746
        ],
747
        'CUBEMEMBER' => [
748
            'category' => Category::CATEGORY_CUBE,
749
            'functionCall' => [Functions::class, 'DUMMY'],
750
            'argumentCount' => '?',
751
        ],
752
        'CUBEMEMBERPROPERTY' => [
753
            'category' => Category::CATEGORY_CUBE,
754
            'functionCall' => [Functions::class, 'DUMMY'],
755
            'argumentCount' => '?',
756
        ],
757
        'CUBERANKEDMEMBER' => [
758
            'category' => Category::CATEGORY_CUBE,
759
            'functionCall' => [Functions::class, 'DUMMY'],
760
            'argumentCount' => '?',
761
        ],
762
        'CUBESET' => [
763
            'category' => Category::CATEGORY_CUBE,
764
            'functionCall' => [Functions::class, 'DUMMY'],
765
            'argumentCount' => '?',
766
        ],
767
        'CUBESETCOUNT' => [
768
            'category' => Category::CATEGORY_CUBE,
769
            'functionCall' => [Functions::class, 'DUMMY'],
770
            'argumentCount' => '?',
771
        ],
772
        'CUBEVALUE' => [
773
            'category' => Category::CATEGORY_CUBE,
774
            'functionCall' => [Functions::class, 'DUMMY'],
775
            'argumentCount' => '?',
776
        ],
777
        'CUMIPMT' => [
778
            'category' => Category::CATEGORY_FINANCIAL,
779
            'functionCall' => [Financial\CashFlow\Constant\Periodic\Cumulative::class, 'interest'],
780
            'argumentCount' => '6',
781
        ],
782
        'CUMPRINC' => [
783
            'category' => Category::CATEGORY_FINANCIAL,
784
            'functionCall' => [Financial\CashFlow\Constant\Periodic\Cumulative::class, 'principal'],
785
            'argumentCount' => '6',
786
        ],
787
        'DATE' => [
788
            'category' => Category::CATEGORY_DATE_AND_TIME,
789
            'functionCall' => [DateTimeExcel\Date::class, 'fromYMD'],
790
            'argumentCount' => '3',
791
        ],
792
        'DATEDIF' => [
793
            'category' => Category::CATEGORY_DATE_AND_TIME,
794
            'functionCall' => [DateTimeExcel\Difference::class, 'interval'],
795
            'argumentCount' => '2,3',
796
        ],
797
        'DATESTRING' => [
798
            'category' => Category::CATEGORY_DATE_AND_TIME,
799
            'functionCall' => [Functions::class, 'DUMMY'],
800
            'argumentCount' => '?',
801
        ],
802
        'DATEVALUE' => [
803
            'category' => Category::CATEGORY_DATE_AND_TIME,
804
            'functionCall' => [DateTimeExcel\DateValue::class, 'fromString'],
805
            'argumentCount' => '1',
806
        ],
807
        'DAVERAGE' => [
808
            'category' => Category::CATEGORY_DATABASE,
809
            'functionCall' => [Database\DAverage::class, 'evaluate'],
810
            'argumentCount' => '3',
811
        ],
812
        'DAY' => [
813
            'category' => Category::CATEGORY_DATE_AND_TIME,
814
            'functionCall' => [DateTimeExcel\DateParts::class, 'day'],
815
            'argumentCount' => '1',
816
        ],
817
        'DAYS' => [
818
            'category' => Category::CATEGORY_DATE_AND_TIME,
819
            'functionCall' => [DateTimeExcel\Days::class, 'between'],
820
            'argumentCount' => '2',
821
        ],
822
        'DAYS360' => [
823
            'category' => Category::CATEGORY_DATE_AND_TIME,
824
            'functionCall' => [DateTimeExcel\Days360::class, 'between'],
825
            'argumentCount' => '2,3',
826
        ],
827
        'DB' => [
828
            'category' => Category::CATEGORY_FINANCIAL,
829
            'functionCall' => [Financial\Depreciation::class, 'DB'],
830
            'argumentCount' => '4,5',
831
        ],
832
        'DBCS' => [
833
            'category' => Category::CATEGORY_TEXT_AND_DATA,
834
            'functionCall' => [Functions::class, 'DUMMY'],
835
            'argumentCount' => '1',
836
        ],
837
        'DCOUNT' => [
838
            'category' => Category::CATEGORY_DATABASE,
839
            'functionCall' => [Database\DCount::class, 'evaluate'],
840
            'argumentCount' => '3',
841
        ],
842
        'DCOUNTA' => [
843
            'category' => Category::CATEGORY_DATABASE,
844
            'functionCall' => [Database\DCountA::class, 'evaluate'],
845
            'argumentCount' => '3',
846
        ],
847
        'DDB' => [
848
            'category' => Category::CATEGORY_FINANCIAL,
849
            'functionCall' => [Financial\Depreciation::class, 'DDB'],
850
            'argumentCount' => '4,5',
851
        ],
852
        'DEC2BIN' => [
853
            'category' => Category::CATEGORY_ENGINEERING,
854
            'functionCall' => [Engineering\ConvertDecimal::class, 'toBinary'],
855
            'argumentCount' => '1,2',
856
        ],
857
        'DEC2HEX' => [
858
            'category' => Category::CATEGORY_ENGINEERING,
859
            'functionCall' => [Engineering\ConvertDecimal::class, 'toHex'],
860
            'argumentCount' => '1,2',
861
        ],
862
        'DEC2OCT' => [
863
            'category' => Category::CATEGORY_ENGINEERING,
864
            'functionCall' => [Engineering\ConvertDecimal::class, 'toOctal'],
865
            'argumentCount' => '1,2',
866
        ],
867
        'DECIMAL' => [
868
            'category' => Category::CATEGORY_MATH_AND_TRIG,
869
            'functionCall' => [Functions::class, 'DUMMY'],
870
            'argumentCount' => '2',
871
        ],
872
        'DEGREES' => [
873
            'category' => Category::CATEGORY_MATH_AND_TRIG,
874
            'functionCall' => [MathTrig\Angle::class, 'toDegrees'],
875
            'argumentCount' => '1',
876
        ],
877
        'DELTA' => [
878
            'category' => Category::CATEGORY_ENGINEERING,
879
            'functionCall' => [Engineering\Compare::class, 'DELTA'],
880
            'argumentCount' => '1,2',
881
        ],
882
        'DEVSQ' => [
883
            'category' => Category::CATEGORY_STATISTICAL,
884
            'functionCall' => [Statistical\Deviations::class, 'sumSquares'],
885
            'argumentCount' => '1+',
886
        ],
887
        'DGET' => [
888
            'category' => Category::CATEGORY_DATABASE,
889
            'functionCall' => [Database\DGet::class, 'evaluate'],
890
            'argumentCount' => '3',
891
        ],
892
        'DISC' => [
893
            'category' => Category::CATEGORY_FINANCIAL,
894
            'functionCall' => [Financial\Securities\Rates::class, 'discount'],
895
            'argumentCount' => '4,5',
896
        ],
897
        'DMAX' => [
898
            'category' => Category::CATEGORY_DATABASE,
899
            'functionCall' => [Database\DMax::class, 'evaluate'],
900
            'argumentCount' => '3',
901
        ],
902
        'DMIN' => [
903
            'category' => Category::CATEGORY_DATABASE,
904
            'functionCall' => [Database\DMin::class, 'evaluate'],
905
            'argumentCount' => '3',
906
        ],
907
        'DOLLAR' => [
908
            'category' => Category::CATEGORY_TEXT_AND_DATA,
909
            'functionCall' => [TextData\Format::class, 'DOLLAR'],
910
            'argumentCount' => '1,2',
911
        ],
912
        'DOLLARDE' => [
913
            'category' => Category::CATEGORY_FINANCIAL,
914
            'functionCall' => [Financial\Dollar::class, 'decimal'],
915
            'argumentCount' => '2',
916
        ],
917
        'DOLLARFR' => [
918
            'category' => Category::CATEGORY_FINANCIAL,
919
            'functionCall' => [Financial\Dollar::class, 'fractional'],
920
            'argumentCount' => '2',
921
        ],
922
        'DPRODUCT' => [
923
            'category' => Category::CATEGORY_DATABASE,
924
            'functionCall' => [Database\DProduct::class, 'evaluate'],
925
            'argumentCount' => '3',
926
        ],
927
        'DROP' => [
928
            'category' => Category::CATEGORY_MATH_AND_TRIG,
929
            'functionCall' => [Functions::class, 'DUMMY'],
930
            'argumentCount' => '2-3',
931
        ],
932
        'DSTDEV' => [
933
            'category' => Category::CATEGORY_DATABASE,
934
            'functionCall' => [Database\DStDev::class, 'evaluate'],
935
            'argumentCount' => '3',
936
        ],
937
        'DSTDEVP' => [
938
            'category' => Category::CATEGORY_DATABASE,
939
            'functionCall' => [Database\DStDevP::class, 'evaluate'],
940
            'argumentCount' => '3',
941
        ],
942
        'DSUM' => [
943
            'category' => Category::CATEGORY_DATABASE,
944
            'functionCall' => [Database\DSum::class, 'evaluate'],
945
            'argumentCount' => '3',
946
        ],
947
        'DURATION' => [
948
            'category' => Category::CATEGORY_FINANCIAL,
949
            'functionCall' => [Functions::class, 'DUMMY'],
950
            'argumentCount' => '5,6',
951
        ],
952
        'DVAR' => [
953
            'category' => Category::CATEGORY_DATABASE,
954
            'functionCall' => [Database\DVar::class, 'evaluate'],
955
            'argumentCount' => '3',
956
        ],
957
        'DVARP' => [
958
            'category' => Category::CATEGORY_DATABASE,
959
            'functionCall' => [Database\DVarP::class, 'evaluate'],
960
            'argumentCount' => '3',
961
        ],
962
        'ECMA.CEILING' => [
963
            'category' => Category::CATEGORY_MATH_AND_TRIG,
964
            'functionCall' => [Functions::class, 'DUMMY'],
965
            'argumentCount' => '1,2',
966
        ],
967
        'EDATE' => [
968
            'category' => Category::CATEGORY_DATE_AND_TIME,
969
            'functionCall' => [DateTimeExcel\Month::class, 'adjust'],
970
            'argumentCount' => '2',
971
        ],
972
        'EFFECT' => [
973
            'category' => Category::CATEGORY_FINANCIAL,
974
            'functionCall' => [Financial\InterestRate::class, 'effective'],
975
            'argumentCount' => '2',
976
        ],
977
        'ENCODEURL' => [
978
            'category' => Category::CATEGORY_WEB,
979
            'functionCall' => [Web\Service::class, 'urlEncode'],
980
            'argumentCount' => '1',
981
        ],
982
        'EOMONTH' => [
983
            'category' => Category::CATEGORY_DATE_AND_TIME,
984
            'functionCall' => [DateTimeExcel\Month::class, 'lastDay'],
985
            'argumentCount' => '2',
986
        ],
987
        'ERF' => [
988
            'category' => Category::CATEGORY_ENGINEERING,
989
            'functionCall' => [Engineering\Erf::class, 'ERF'],
990
            'argumentCount' => '1,2',
991
        ],
992
        'ERF.PRECISE' => [
993
            'category' => Category::CATEGORY_ENGINEERING,
994
            'functionCall' => [Engineering\Erf::class, 'ERFPRECISE'],
995
            'argumentCount' => '1',
996
        ],
997
        'ERFC' => [
998
            'category' => Category::CATEGORY_ENGINEERING,
999
            'functionCall' => [Engineering\ErfC::class, 'ERFC'],
1000
            'argumentCount' => '1',
1001
        ],
1002
        'ERFC.PRECISE' => [
1003
            'category' => Category::CATEGORY_ENGINEERING,
1004
            'functionCall' => [Engineering\ErfC::class, 'ERFC'],
1005
            'argumentCount' => '1',
1006
        ],
1007
        'ERROR.TYPE' => [
1008
            'category' => Category::CATEGORY_INFORMATION,
1009
            'functionCall' => [ExcelError::class, 'type'],
1010
            'argumentCount' => '1',
1011
        ],
1012
        'EVEN' => [
1013
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1014
            'functionCall' => [MathTrig\Round::class, 'even'],
1015
            'argumentCount' => '1',
1016
        ],
1017
        'EXACT' => [
1018
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1019
            'functionCall' => [TextData\Text::class, 'exact'],
1020
            'argumentCount' => '2',
1021
        ],
1022
        'EXP' => [
1023
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1024
            'functionCall' => [MathTrig\Exp::class, 'evaluate'],
1025
            'argumentCount' => '1',
1026
        ],
1027
        'EXPAND' => [
1028
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1029
            'functionCall' => [Functions::class, 'DUMMY'],
1030
            'argumentCount' => '2-4',
1031
        ],
1032
        'EXPONDIST' => [
1033
            'category' => Category::CATEGORY_STATISTICAL,
1034
            'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'],
1035
            'argumentCount' => '3',
1036
        ],
1037
        'EXPON.DIST' => [
1038
            'category' => Category::CATEGORY_STATISTICAL,
1039
            'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'],
1040
            'argumentCount' => '3',
1041
        ],
1042
        'FACT' => [
1043
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1044
            'functionCall' => [MathTrig\Factorial::class, 'fact'],
1045
            'argumentCount' => '1',
1046
        ],
1047
        'FACTDOUBLE' => [
1048
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1049
            'functionCall' => [MathTrig\Factorial::class, 'factDouble'],
1050
            'argumentCount' => '1',
1051
        ],
1052
        'FALSE' => [
1053
            'category' => Category::CATEGORY_LOGICAL,
1054
            'functionCall' => [Logical\Boolean::class, 'FALSE'],
1055
            'argumentCount' => '0',
1056
        ],
1057
        'FDIST' => [
1058
            'category' => Category::CATEGORY_STATISTICAL,
1059
            'functionCall' => [Functions::class, 'DUMMY'],
1060
            'argumentCount' => '3',
1061
        ],
1062
        'F.DIST' => [
1063
            'category' => Category::CATEGORY_STATISTICAL,
1064
            'functionCall' => [Statistical\Distributions\F::class, 'distribution'],
1065
            'argumentCount' => '4',
1066
        ],
1067
        'F.DIST.RT' => [
1068
            'category' => Category::CATEGORY_STATISTICAL,
1069
            'functionCall' => [Functions::class, 'DUMMY'],
1070
            'argumentCount' => '3',
1071
        ],
1072
        'FILTER' => [
1073
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1074
            'functionCall' => [LookupRef\Filter::class, 'filter'],
1075
            'argumentCount' => '2-3',
1076
        ],
1077
        'FILTERXML' => [
1078
            'category' => Category::CATEGORY_WEB,
1079
            'functionCall' => [Functions::class, 'DUMMY'],
1080
            'argumentCount' => '2',
1081
        ],
1082
        'FIND' => [
1083
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1084
            'functionCall' => [TextData\Search::class, 'sensitive'],
1085
            'argumentCount' => '2,3',
1086
        ],
1087
        'FINDB' => [
1088
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1089
            'functionCall' => [TextData\Search::class, 'sensitive'],
1090
            'argumentCount' => '2,3',
1091
        ],
1092
        'FINV' => [
1093
            'category' => Category::CATEGORY_STATISTICAL,
1094
            'functionCall' => [Functions::class, 'DUMMY'],
1095
            'argumentCount' => '3',
1096
        ],
1097
        'F.INV' => [
1098
            'category' => Category::CATEGORY_STATISTICAL,
1099
            'functionCall' => [Functions::class, 'DUMMY'],
1100
            'argumentCount' => '3',
1101
        ],
1102
        'F.INV.RT' => [
1103
            'category' => Category::CATEGORY_STATISTICAL,
1104
            'functionCall' => [Functions::class, 'DUMMY'],
1105
            'argumentCount' => '3',
1106
        ],
1107
        'FISHER' => [
1108
            'category' => Category::CATEGORY_STATISTICAL,
1109
            'functionCall' => [Statistical\Distributions\Fisher::class, 'distribution'],
1110
            'argumentCount' => '1',
1111
        ],
1112
        'FISHERINV' => [
1113
            'category' => Category::CATEGORY_STATISTICAL,
1114
            'functionCall' => [Statistical\Distributions\Fisher::class, 'inverse'],
1115
            'argumentCount' => '1',
1116
        ],
1117
        'FIXED' => [
1118
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1119
            'functionCall' => [TextData\Format::class, 'FIXEDFORMAT'],
1120
            'argumentCount' => '1-3',
1121
        ],
1122
        'FLOOR' => [
1123
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1124
            'functionCall' => [MathTrig\Floor::class, 'floor'],
1125
            'argumentCount' => '1-2', // Excel requries 2, Ods/Gnumeric 1-2
1126
        ],
1127
        'FLOOR.MATH' => [
1128
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1129
            'functionCall' => [MathTrig\Floor::class, 'math'],
1130
            'argumentCount' => '1-3',
1131
        ],
1132
        'FLOOR.PRECISE' => [
1133
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1134
            'functionCall' => [MathTrig\Floor::class, 'precise'],
1135
            'argumentCount' => '1-2',
1136
        ],
1137
        'FORECAST' => [
1138
            'category' => Category::CATEGORY_STATISTICAL,
1139
            'functionCall' => [Statistical\Trends::class, 'FORECAST'],
1140
            'argumentCount' => '3',
1141
        ],
1142
        'FORECAST.ETS' => [
1143
            'category' => Category::CATEGORY_STATISTICAL,
1144
            'functionCall' => [Functions::class, 'DUMMY'],
1145
            'argumentCount' => '3-6',
1146
        ],
1147
        'FORECAST.ETS.CONFINT' => [
1148
            'category' => Category::CATEGORY_STATISTICAL,
1149
            'functionCall' => [Functions::class, 'DUMMY'],
1150
            'argumentCount' => '3-6',
1151
        ],
1152
        'FORECAST.ETS.SEASONALITY' => [
1153
            'category' => Category::CATEGORY_STATISTICAL,
1154
            'functionCall' => [Functions::class, 'DUMMY'],
1155
            'argumentCount' => '2-4',
1156
        ],
1157
        'FORECAST.ETS.STAT' => [
1158
            'category' => Category::CATEGORY_STATISTICAL,
1159
            'functionCall' => [Functions::class, 'DUMMY'],
1160
            'argumentCount' => '3-6',
1161
        ],
1162
        'FORECAST.LINEAR' => [
1163
            'category' => Category::CATEGORY_STATISTICAL,
1164
            'functionCall' => [Statistical\Trends::class, 'FORECAST'],
1165
            'argumentCount' => '3',
1166
        ],
1167
        'FORMULATEXT' => [
1168
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1169
            'functionCall' => [LookupRef\Formula::class, 'text'],
1170
            'argumentCount' => '1',
1171
            'passCellReference' => true,
1172
            'passByReference' => [true],
1173
        ],
1174
        'FREQUENCY' => [
1175
            'category' => Category::CATEGORY_STATISTICAL,
1176
            'functionCall' => [Functions::class, 'DUMMY'],
1177
            'argumentCount' => '2',
1178
        ],
1179
        'FTEST' => [
1180
            'category' => Category::CATEGORY_STATISTICAL,
1181
            'functionCall' => [Functions::class, 'DUMMY'],
1182
            'argumentCount' => '2',
1183
        ],
1184
        'F.TEST' => [
1185
            'category' => Category::CATEGORY_STATISTICAL,
1186
            'functionCall' => [Functions::class, 'DUMMY'],
1187
            'argumentCount' => '2',
1188
        ],
1189
        'FV' => [
1190
            'category' => Category::CATEGORY_FINANCIAL,
1191
            'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'futureValue'],
1192
            'argumentCount' => '3-5',
1193
        ],
1194
        'FVSCHEDULE' => [
1195
            'category' => Category::CATEGORY_FINANCIAL,
1196
            'functionCall' => [Financial\CashFlow\Single::class, 'futureValue'],
1197
            'argumentCount' => '2',
1198
        ],
1199
        'GAMMA' => [
1200
            'category' => Category::CATEGORY_STATISTICAL,
1201
            'functionCall' => [Statistical\Distributions\Gamma::class, 'gamma'],
1202
            'argumentCount' => '1',
1203
        ],
1204
        'GAMMADIST' => [
1205
            'category' => Category::CATEGORY_STATISTICAL,
1206
            'functionCall' => [Statistical\Distributions\Gamma::class, 'distribution'],
1207
            'argumentCount' => '4',
1208
        ],
1209
        'GAMMA.DIST' => [
1210
            'category' => Category::CATEGORY_STATISTICAL,
1211
            'functionCall' => [Statistical\Distributions\Gamma::class, 'distribution'],
1212
            'argumentCount' => '4',
1213
        ],
1214
        'GAMMAINV' => [
1215
            'category' => Category::CATEGORY_STATISTICAL,
1216
            'functionCall' => [Statistical\Distributions\Gamma::class, 'inverse'],
1217
            'argumentCount' => '3',
1218
        ],
1219
        'GAMMA.INV' => [
1220
            'category' => Category::CATEGORY_STATISTICAL,
1221
            'functionCall' => [Statistical\Distributions\Gamma::class, 'inverse'],
1222
            'argumentCount' => '3',
1223
        ],
1224
        'GAMMALN' => [
1225
            'category' => Category::CATEGORY_STATISTICAL,
1226
            'functionCall' => [Statistical\Distributions\Gamma::class, 'ln'],
1227
            'argumentCount' => '1',
1228
        ],
1229
        'GAMMALN.PRECISE' => [
1230
            'category' => Category::CATEGORY_STATISTICAL,
1231
            'functionCall' => [Statistical\Distributions\Gamma::class, 'ln'],
1232
            'argumentCount' => '1',
1233
        ],
1234
        'GAUSS' => [
1235
            'category' => Category::CATEGORY_STATISTICAL,
1236
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'gauss'],
1237
            'argumentCount' => '1',
1238
        ],
1239
        'GCD' => [
1240
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1241
            'functionCall' => [MathTrig\Gcd::class, 'evaluate'],
1242
            'argumentCount' => '1+',
1243
        ],
1244
        'GEOMEAN' => [
1245
            'category' => Category::CATEGORY_STATISTICAL,
1246
            'functionCall' => [Statistical\Averages\Mean::class, 'geometric'],
1247
            'argumentCount' => '1+',
1248
        ],
1249
        'GESTEP' => [
1250
            'category' => Category::CATEGORY_ENGINEERING,
1251
            'functionCall' => [Engineering\Compare::class, 'GESTEP'],
1252
            'argumentCount' => '1,2',
1253
        ],
1254
        'GETPIVOTDATA' => [
1255
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1256
            'functionCall' => [Functions::class, 'DUMMY'],
1257
            'argumentCount' => '2+',
1258
        ],
1259
        'GROWTH' => [
1260
            'category' => Category::CATEGORY_STATISTICAL,
1261
            'functionCall' => [Statistical\Trends::class, 'GROWTH'],
1262
            'argumentCount' => '1-4',
1263
        ],
1264
        'HARMEAN' => [
1265
            'category' => Category::CATEGORY_STATISTICAL,
1266
            'functionCall' => [Statistical\Averages\Mean::class, 'harmonic'],
1267
            'argumentCount' => '1+',
1268
        ],
1269
        'HEX2BIN' => [
1270
            'category' => Category::CATEGORY_ENGINEERING,
1271
            'functionCall' => [Engineering\ConvertHex::class, 'toBinary'],
1272
            'argumentCount' => '1,2',
1273
        ],
1274
        'HEX2DEC' => [
1275
            'category' => Category::CATEGORY_ENGINEERING,
1276
            'functionCall' => [Engineering\ConvertHex::class, 'toDecimal'],
1277
            'argumentCount' => '1',
1278
        ],
1279
        'HEX2OCT' => [
1280
            'category' => Category::CATEGORY_ENGINEERING,
1281
            'functionCall' => [Engineering\ConvertHex::class, 'toOctal'],
1282
            'argumentCount' => '1,2',
1283
        ],
1284
        'HLOOKUP' => [
1285
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1286
            'functionCall' => [LookupRef\HLookup::class, 'lookup'],
1287
            'argumentCount' => '3,4',
1288
        ],
1289
        'HOUR' => [
1290
            'category' => Category::CATEGORY_DATE_AND_TIME,
1291
            'functionCall' => [DateTimeExcel\TimeParts::class, 'hour'],
1292
            'argumentCount' => '1',
1293
        ],
1294
        'HSTACK' => [
1295
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1296
            'functionCall' => [Functions::class, 'DUMMY'],
1297
            'argumentCount' => '1+',
1298
        ],
1299
        'HYPERLINK' => [
1300
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1301
            'functionCall' => [LookupRef\Hyperlink::class, 'set'],
1302
            'argumentCount' => '1,2',
1303
            'passCellReference' => true,
1304
        ],
1305
        'HYPGEOMDIST' => [
1306
            'category' => Category::CATEGORY_STATISTICAL,
1307
            'functionCall' => [Statistical\Distributions\HyperGeometric::class, 'distribution'],
1308
            'argumentCount' => '4',
1309
        ],
1310
        'HYPGEOM.DIST' => [
1311
            'category' => Category::CATEGORY_STATISTICAL,
1312
            'functionCall' => [Functions::class, 'DUMMY'],
1313
            'argumentCount' => '5',
1314
        ],
1315
        'IF' => [
1316
            'category' => Category::CATEGORY_LOGICAL,
1317
            'functionCall' => [Logical\Conditional::class, 'statementIf'],
1318
            'argumentCount' => '2-3',
1319
        ],
1320
        'IFERROR' => [
1321
            'category' => Category::CATEGORY_LOGICAL,
1322
            'functionCall' => [Logical\Conditional::class, 'IFERROR'],
1323
            'argumentCount' => '2',
1324
        ],
1325
        'IFNA' => [
1326
            'category' => Category::CATEGORY_LOGICAL,
1327
            'functionCall' => [Logical\Conditional::class, 'IFNA'],
1328
            'argumentCount' => '2',
1329
        ],
1330
        'IFS' => [
1331
            'category' => Category::CATEGORY_LOGICAL,
1332
            'functionCall' => [Logical\Conditional::class, 'IFS'],
1333
            'argumentCount' => '2+',
1334
        ],
1335
        'IMABS' => [
1336
            'category' => Category::CATEGORY_ENGINEERING,
1337
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMABS'],
1338
            'argumentCount' => '1',
1339
        ],
1340
        'IMAGINARY' => [
1341
            'category' => Category::CATEGORY_ENGINEERING,
1342
            'functionCall' => [Engineering\Complex::class, 'IMAGINARY'],
1343
            'argumentCount' => '1',
1344
        ],
1345
        'IMARGUMENT' => [
1346
            'category' => Category::CATEGORY_ENGINEERING,
1347
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMARGUMENT'],
1348
            'argumentCount' => '1',
1349
        ],
1350
        'IMCONJUGATE' => [
1351
            'category' => Category::CATEGORY_ENGINEERING,
1352
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMCONJUGATE'],
1353
            'argumentCount' => '1',
1354
        ],
1355
        'IMCOS' => [
1356
            'category' => Category::CATEGORY_ENGINEERING,
1357
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOS'],
1358
            'argumentCount' => '1',
1359
        ],
1360
        'IMCOSH' => [
1361
            'category' => Category::CATEGORY_ENGINEERING,
1362
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOSH'],
1363
            'argumentCount' => '1',
1364
        ],
1365
        'IMCOT' => [
1366
            'category' => Category::CATEGORY_ENGINEERING,
1367
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOT'],
1368
            'argumentCount' => '1',
1369
        ],
1370
        'IMCSC' => [
1371
            'category' => Category::CATEGORY_ENGINEERING,
1372
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMCSC'],
1373
            'argumentCount' => '1',
1374
        ],
1375
        'IMCSCH' => [
1376
            'category' => Category::CATEGORY_ENGINEERING,
1377
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMCSCH'],
1378
            'argumentCount' => '1',
1379
        ],
1380
        'IMDIV' => [
1381
            'category' => Category::CATEGORY_ENGINEERING,
1382
            'functionCall' => [Engineering\ComplexOperations::class, 'IMDIV'],
1383
            'argumentCount' => '2',
1384
        ],
1385
        'IMEXP' => [
1386
            'category' => Category::CATEGORY_ENGINEERING,
1387
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMEXP'],
1388
            'argumentCount' => '1',
1389
        ],
1390
        'IMLN' => [
1391
            'category' => Category::CATEGORY_ENGINEERING,
1392
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMLN'],
1393
            'argumentCount' => '1',
1394
        ],
1395
        'IMLOG10' => [
1396
            'category' => Category::CATEGORY_ENGINEERING,
1397
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMLOG10'],
1398
            'argumentCount' => '1',
1399
        ],
1400
        'IMLOG2' => [
1401
            'category' => Category::CATEGORY_ENGINEERING,
1402
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMLOG2'],
1403
            'argumentCount' => '1',
1404
        ],
1405
        'IMPOWER' => [
1406
            'category' => Category::CATEGORY_ENGINEERING,
1407
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMPOWER'],
1408
            'argumentCount' => '2',
1409
        ],
1410
        'IMPRODUCT' => [
1411
            'category' => Category::CATEGORY_ENGINEERING,
1412
            'functionCall' => [Engineering\ComplexOperations::class, 'IMPRODUCT'],
1413
            'argumentCount' => '1+',
1414
        ],
1415
        'IMREAL' => [
1416
            'category' => Category::CATEGORY_ENGINEERING,
1417
            'functionCall' => [Engineering\Complex::class, 'IMREAL'],
1418
            'argumentCount' => '1',
1419
        ],
1420
        'IMSEC' => [
1421
            'category' => Category::CATEGORY_ENGINEERING,
1422
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMSEC'],
1423
            'argumentCount' => '1',
1424
        ],
1425
        'IMSECH' => [
1426
            'category' => Category::CATEGORY_ENGINEERING,
1427
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMSECH'],
1428
            'argumentCount' => '1',
1429
        ],
1430
        'IMSIN' => [
1431
            'category' => Category::CATEGORY_ENGINEERING,
1432
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMSIN'],
1433
            'argumentCount' => '1',
1434
        ],
1435
        'IMSINH' => [
1436
            'category' => Category::CATEGORY_ENGINEERING,
1437
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMSINH'],
1438
            'argumentCount' => '1',
1439
        ],
1440
        'IMSQRT' => [
1441
            'category' => Category::CATEGORY_ENGINEERING,
1442
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMSQRT'],
1443
            'argumentCount' => '1',
1444
        ],
1445
        'IMSUB' => [
1446
            'category' => Category::CATEGORY_ENGINEERING,
1447
            'functionCall' => [Engineering\ComplexOperations::class, 'IMSUB'],
1448
            'argumentCount' => '2',
1449
        ],
1450
        'IMSUM' => [
1451
            'category' => Category::CATEGORY_ENGINEERING,
1452
            'functionCall' => [Engineering\ComplexOperations::class, 'IMSUM'],
1453
            'argumentCount' => '1+',
1454
        ],
1455
        'IMTAN' => [
1456
            'category' => Category::CATEGORY_ENGINEERING,
1457
            'functionCall' => [Engineering\ComplexFunctions::class, 'IMTAN'],
1458
            'argumentCount' => '1',
1459
        ],
1460
        'INDEX' => [
1461
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1462
            'functionCall' => [LookupRef\Matrix::class, 'index'],
1463
            'argumentCount' => '2-4',
1464
        ],
1465
        'INDIRECT' => [
1466
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1467
            'functionCall' => [LookupRef\Indirect::class, 'INDIRECT'],
1468
            'argumentCount' => '1,2',
1469
            'passCellReference' => true,
1470
        ],
1471
        'INFO' => [
1472
            'category' => Category::CATEGORY_INFORMATION,
1473
            'functionCall' => [Functions::class, 'DUMMY'],
1474
            'argumentCount' => '1',
1475
        ],
1476
        'INT' => [
1477
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1478
            'functionCall' => [MathTrig\IntClass::class, 'evaluate'],
1479
            'argumentCount' => '1',
1480
        ],
1481
        'INTERCEPT' => [
1482
            'category' => Category::CATEGORY_STATISTICAL,
1483
            'functionCall' => [Statistical\Trends::class, 'INTERCEPT'],
1484
            'argumentCount' => '2',
1485
        ],
1486
        'INTRATE' => [
1487
            'category' => Category::CATEGORY_FINANCIAL,
1488
            'functionCall' => [Financial\Securities\Rates::class, 'interest'],
1489
            'argumentCount' => '4,5',
1490
        ],
1491
        'IPMT' => [
1492
            'category' => Category::CATEGORY_FINANCIAL,
1493
            'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'payment'],
1494
            'argumentCount' => '4-6',
1495
        ],
1496
        'IRR' => [
1497
            'category' => Category::CATEGORY_FINANCIAL,
1498
            'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'rate'],
1499
            'argumentCount' => '1,2',
1500
        ],
1501
        'ISBLANK' => [
1502
            'category' => Category::CATEGORY_INFORMATION,
1503
            'functionCall' => [Information\Value::class, 'isBlank'],
1504
            'argumentCount' => '1',
1505
        ],
1506
        'ISERR' => [
1507
            'category' => Category::CATEGORY_INFORMATION,
1508
            'functionCall' => [Information\ErrorValue::class, 'isErr'],
1509
            'argumentCount' => '1',
1510
        ],
1511
        'ISERROR' => [
1512
            'category' => Category::CATEGORY_INFORMATION,
1513
            'functionCall' => [Information\ErrorValue::class, 'isError'],
1514
            'argumentCount' => '1',
1515
        ],
1516
        'ISEVEN' => [
1517
            'category' => Category::CATEGORY_INFORMATION,
1518
            'functionCall' => [Information\Value::class, 'isEven'],
1519
            'argumentCount' => '1',
1520
        ],
1521
        'ISFORMULA' => [
1522
            'category' => Category::CATEGORY_INFORMATION,
1523
            'functionCall' => [Information\Value::class, 'isFormula'],
1524
            'argumentCount' => '1',
1525
            'passCellReference' => true,
1526
            'passByReference' => [true],
1527
        ],
1528
        'ISLOGICAL' => [
1529
            'category' => Category::CATEGORY_INFORMATION,
1530
            'functionCall' => [Information\Value::class, 'isLogical'],
1531
            'argumentCount' => '1',
1532
        ],
1533
        'ISNA' => [
1534
            'category' => Category::CATEGORY_INFORMATION,
1535
            'functionCall' => [Information\ErrorValue::class, 'isNa'],
1536
            'argumentCount' => '1',
1537
        ],
1538
        'ISNONTEXT' => [
1539
            'category' => Category::CATEGORY_INFORMATION,
1540
            'functionCall' => [Information\Value::class, 'isNonText'],
1541
            'argumentCount' => '1',
1542
        ],
1543
        'ISNUMBER' => [
1544
            'category' => Category::CATEGORY_INFORMATION,
1545
            'functionCall' => [Information\Value::class, 'isNumber'],
1546
            'argumentCount' => '1',
1547
        ],
1548
        'ISO.CEILING' => [
1549
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1550
            'functionCall' => [Functions::class, 'DUMMY'],
1551
            'argumentCount' => '1,2',
1552
        ],
1553
        'ISODD' => [
1554
            'category' => Category::CATEGORY_INFORMATION,
1555
            'functionCall' => [Information\Value::class, 'isOdd'],
1556
            'argumentCount' => '1',
1557
        ],
1558
        'ISOMITTED' => [
1559
            'category' => Category::CATEGORY_INFORMATION,
1560
            'functionCall' => [Functions::class, 'DUMMY'],
1561
            'argumentCount' => '*',
1562
        ],
1563
        'ISOWEEKNUM' => [
1564
            'category' => Category::CATEGORY_DATE_AND_TIME,
1565
            'functionCall' => [DateTimeExcel\Week::class, 'isoWeekNumber'],
1566
            'argumentCount' => '1',
1567
        ],
1568
        'ISPMT' => [
1569
            'category' => Category::CATEGORY_FINANCIAL,
1570
            'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'schedulePayment'],
1571
            'argumentCount' => '4',
1572
        ],
1573
        'ISREF' => [
1574
            'category' => Category::CATEGORY_INFORMATION,
1575
            'functionCall' => [Information\Value::class, 'isRef'],
1576
            'argumentCount' => '1',
1577
            'passCellReference' => true,
1578
            'passByReference' => [true],
1579
        ],
1580
        'ISTEXT' => [
1581
            'category' => Category::CATEGORY_INFORMATION,
1582
            'functionCall' => [Information\Value::class, 'isText'],
1583
            'argumentCount' => '1',
1584
        ],
1585
        'ISTHAIDIGIT' => [
1586
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1587
            'functionCall' => [Functions::class, 'DUMMY'],
1588
            'argumentCount' => '?',
1589
        ],
1590
        'JIS' => [
1591
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1592
            'functionCall' => [Functions::class, 'DUMMY'],
1593
            'argumentCount' => '1',
1594
        ],
1595
        'KURT' => [
1596
            'category' => Category::CATEGORY_STATISTICAL,
1597
            'functionCall' => [Statistical\Deviations::class, 'kurtosis'],
1598
            'argumentCount' => '1+',
1599
        ],
1600
        'LAMBDA' => [
1601
            'category' => Category::CATEGORY_LOGICAL,
1602
            'functionCall' => [Functions::class, 'DUMMY'],
1603
            'argumentCount' => '*',
1604
        ],
1605
        'LARGE' => [
1606
            'category' => Category::CATEGORY_STATISTICAL,
1607
            'functionCall' => [Statistical\Size::class, 'large'],
1608
            'argumentCount' => '2',
1609
        ],
1610
        'LCM' => [
1611
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1612
            'functionCall' => [MathTrig\Lcm::class, 'evaluate'],
1613
            'argumentCount' => '1+',
1614
        ],
1615
        'LEFT' => [
1616
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1617
            'functionCall' => [TextData\Extract::class, 'left'],
1618
            'argumentCount' => '1,2',
1619
        ],
1620
        'LEFTB' => [
1621
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1622
            'functionCall' => [TextData\Extract::class, 'left'],
1623
            'argumentCount' => '1,2',
1624
        ],
1625
        'LEN' => [
1626
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1627
            'functionCall' => [TextData\Text::class, 'length'],
1628
            'argumentCount' => '1',
1629
        ],
1630
        'LENB' => [
1631
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1632
            'functionCall' => [TextData\Text::class, 'length'],
1633
            'argumentCount' => '1',
1634
        ],
1635
        'LET' => [
1636
            'category' => Category::CATEGORY_LOGICAL,
1637
            'functionCall' => [Functions::class, 'DUMMY'],
1638
            'argumentCount' => '*',
1639
        ],
1640
        'LINEST' => [
1641
            'category' => Category::CATEGORY_STATISTICAL,
1642
            'functionCall' => [Statistical\Trends::class, 'LINEST'],
1643
            'argumentCount' => '1-4',
1644
        ],
1645
        'LN' => [
1646
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1647
            'functionCall' => [MathTrig\Logarithms::class, 'natural'],
1648
            'argumentCount' => '1',
1649
        ],
1650
        'LOG' => [
1651
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1652
            'functionCall' => [MathTrig\Logarithms::class, 'withBase'],
1653
            'argumentCount' => '1,2',
1654
        ],
1655
        'LOG10' => [
1656
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1657
            'functionCall' => [MathTrig\Logarithms::class, 'base10'],
1658
            'argumentCount' => '1',
1659
        ],
1660
        'LOGEST' => [
1661
            'category' => Category::CATEGORY_STATISTICAL,
1662
            'functionCall' => [Statistical\Trends::class, 'LOGEST'],
1663
            'argumentCount' => '1-4',
1664
        ],
1665
        'LOGINV' => [
1666
            'category' => Category::CATEGORY_STATISTICAL,
1667
            'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'],
1668
            'argumentCount' => '3',
1669
        ],
1670
        'LOGNORMDIST' => [
1671
            'category' => Category::CATEGORY_STATISTICAL,
1672
            'functionCall' => [Statistical\Distributions\LogNormal::class, 'cumulative'],
1673
            'argumentCount' => '3',
1674
        ],
1675
        'LOGNORM.DIST' => [
1676
            'category' => Category::CATEGORY_STATISTICAL,
1677
            'functionCall' => [Statistical\Distributions\LogNormal::class, 'distribution'],
1678
            'argumentCount' => '4',
1679
        ],
1680
        'LOGNORM.INV' => [
1681
            'category' => Category::CATEGORY_STATISTICAL,
1682
            'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'],
1683
            'argumentCount' => '3',
1684
        ],
1685
        'LOOKUP' => [
1686
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1687
            'functionCall' => [LookupRef\Lookup::class, 'lookup'],
1688
            'argumentCount' => '2,3',
1689
        ],
1690
        'LOWER' => [
1691
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1692
            'functionCall' => [TextData\CaseConvert::class, 'lower'],
1693
            'argumentCount' => '1',
1694
        ],
1695
        'MAKEARRAY' => [
1696
            'category' => Category::CATEGORY_LOGICAL,
1697
            'functionCall' => [Functions::class, 'DUMMY'],
1698
            'argumentCount' => '*',
1699
        ],
1700
        'MAP' => [
1701
            'category' => Category::CATEGORY_LOGICAL,
1702
            'functionCall' => [Functions::class, 'DUMMY'],
1703
            'argumentCount' => '*',
1704
        ],
1705
        'MATCH' => [
1706
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1707
            'functionCall' => [LookupRef\ExcelMatch::class, 'MATCH'],
1708
            'argumentCount' => '2,3',
1709
        ],
1710
        'MAX' => [
1711
            'category' => Category::CATEGORY_STATISTICAL,
1712
            'functionCall' => [Statistical\Maximum::class, 'max'],
1713
            'argumentCount' => '1+',
1714
        ],
1715
        'MAXA' => [
1716
            'category' => Category::CATEGORY_STATISTICAL,
1717
            'functionCall' => [Statistical\Maximum::class, 'maxA'],
1718
            'argumentCount' => '1+',
1719
        ],
1720
        'MAXIFS' => [
1721
            'category' => Category::CATEGORY_STATISTICAL,
1722
            'functionCall' => [Statistical\Conditional::class, 'MAXIFS'],
1723
            'argumentCount' => '3+',
1724
        ],
1725
        'MDETERM' => [
1726
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1727
            'functionCall' => [MathTrig\MatrixFunctions::class, 'determinant'],
1728
            'argumentCount' => '1',
1729
        ],
1730
        'MDURATION' => [
1731
            'category' => Category::CATEGORY_FINANCIAL,
1732
            'functionCall' => [Functions::class, 'DUMMY'],
1733
            'argumentCount' => '5,6',
1734
        ],
1735
        'MEDIAN' => [
1736
            'category' => Category::CATEGORY_STATISTICAL,
1737
            'functionCall' => [Statistical\Averages::class, 'median'],
1738
            'argumentCount' => '1+',
1739
        ],
1740
        'MEDIANIF' => [
1741
            'category' => Category::CATEGORY_STATISTICAL,
1742
            'functionCall' => [Functions::class, 'DUMMY'],
1743
            'argumentCount' => '2+',
1744
        ],
1745
        'MID' => [
1746
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1747
            'functionCall' => [TextData\Extract::class, 'mid'],
1748
            'argumentCount' => '3',
1749
        ],
1750
        'MIDB' => [
1751
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1752
            'functionCall' => [TextData\Extract::class, 'mid'],
1753
            'argumentCount' => '3',
1754
        ],
1755
        'MIN' => [
1756
            'category' => Category::CATEGORY_STATISTICAL,
1757
            'functionCall' => [Statistical\Minimum::class, 'min'],
1758
            'argumentCount' => '1+',
1759
        ],
1760
        'MINA' => [
1761
            'category' => Category::CATEGORY_STATISTICAL,
1762
            'functionCall' => [Statistical\Minimum::class, 'minA'],
1763
            'argumentCount' => '1+',
1764
        ],
1765
        'MINIFS' => [
1766
            'category' => Category::CATEGORY_STATISTICAL,
1767
            'functionCall' => [Statistical\Conditional::class, 'MINIFS'],
1768
            'argumentCount' => '3+',
1769
        ],
1770
        'MINUTE' => [
1771
            'category' => Category::CATEGORY_DATE_AND_TIME,
1772
            'functionCall' => [DateTimeExcel\TimeParts::class, 'minute'],
1773
            'argumentCount' => '1',
1774
        ],
1775
        'MINVERSE' => [
1776
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1777
            'functionCall' => [MathTrig\MatrixFunctions::class, 'inverse'],
1778
            'argumentCount' => '1',
1779
        ],
1780
        'MIRR' => [
1781
            'category' => Category::CATEGORY_FINANCIAL,
1782
            'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'modifiedRate'],
1783
            'argumentCount' => '3',
1784
        ],
1785
        'MMULT' => [
1786
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1787
            'functionCall' => [MathTrig\MatrixFunctions::class, 'multiply'],
1788
            'argumentCount' => '2',
1789
        ],
1790
        'MOD' => [
1791
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1792
            'functionCall' => [MathTrig\Operations::class, 'mod'],
1793
            'argumentCount' => '2',
1794
        ],
1795
        'MODE' => [
1796
            'category' => Category::CATEGORY_STATISTICAL,
1797
            'functionCall' => [Statistical\Averages::class, 'mode'],
1798
            'argumentCount' => '1+',
1799
        ],
1800
        'MODE.MULT' => [
1801
            'category' => Category::CATEGORY_STATISTICAL,
1802
            'functionCall' => [Functions::class, 'DUMMY'],
1803
            'argumentCount' => '1+',
1804
        ],
1805
        'MODE.SNGL' => [
1806
            'category' => Category::CATEGORY_STATISTICAL,
1807
            'functionCall' => [Statistical\Averages::class, 'mode'],
1808
            'argumentCount' => '1+',
1809
        ],
1810
        'MONTH' => [
1811
            'category' => Category::CATEGORY_DATE_AND_TIME,
1812
            'functionCall' => [DateTimeExcel\DateParts::class, 'month'],
1813
            'argumentCount' => '1',
1814
        ],
1815
        'MROUND' => [
1816
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1817
            'functionCall' => [MathTrig\Round::class, 'multiple'],
1818
            'argumentCount' => '2',
1819
        ],
1820
        'MULTINOMIAL' => [
1821
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1822
            'functionCall' => [MathTrig\Factorial::class, 'multinomial'],
1823
            'argumentCount' => '1+',
1824
        ],
1825
        'MUNIT' => [
1826
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1827
            'functionCall' => [MathTrig\MatrixFunctions::class, 'identity'],
1828
            'argumentCount' => '1',
1829
        ],
1830
        'N' => [
1831
            'category' => Category::CATEGORY_INFORMATION,
1832
            'functionCall' => [Information\Value::class, 'asNumber'],
1833
            'argumentCount' => '1',
1834
        ],
1835
        'NA' => [
1836
            'category' => Category::CATEGORY_INFORMATION,
1837
            'functionCall' => [ExcelError::class, 'NA'],
1838
            'argumentCount' => '0',
1839
        ],
1840
        'NEGBINOMDIST' => [
1841
            'category' => Category::CATEGORY_STATISTICAL,
1842
            'functionCall' => [Statistical\Distributions\Binomial::class, 'negative'],
1843
            'argumentCount' => '3',
1844
        ],
1845
        'NEGBINOM.DIST' => [
1846
            'category' => Category::CATEGORY_STATISTICAL,
1847
            'functionCall' => [Functions::class, 'DUMMY'],
1848
            'argumentCount' => '4',
1849
        ],
1850
        'NETWORKDAYS' => [
1851
            'category' => Category::CATEGORY_DATE_AND_TIME,
1852
            'functionCall' => [DateTimeExcel\NetworkDays::class, 'count'],
1853
            'argumentCount' => '2-3',
1854
        ],
1855
        'NETWORKDAYS.INTL' => [
1856
            'category' => Category::CATEGORY_DATE_AND_TIME,
1857
            'functionCall' => [Functions::class, 'DUMMY'],
1858
            'argumentCount' => '2-4',
1859
        ],
1860
        'NOMINAL' => [
1861
            'category' => Category::CATEGORY_FINANCIAL,
1862
            'functionCall' => [Financial\InterestRate::class, 'nominal'],
1863
            'argumentCount' => '2',
1864
        ],
1865
        'NORMDIST' => [
1866
            'category' => Category::CATEGORY_STATISTICAL,
1867
            'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'],
1868
            'argumentCount' => '4',
1869
        ],
1870
        'NORM.DIST' => [
1871
            'category' => Category::CATEGORY_STATISTICAL,
1872
            'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'],
1873
            'argumentCount' => '4',
1874
        ],
1875
        'NORMINV' => [
1876
            'category' => Category::CATEGORY_STATISTICAL,
1877
            'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'],
1878
            'argumentCount' => '3',
1879
        ],
1880
        'NORM.INV' => [
1881
            'category' => Category::CATEGORY_STATISTICAL,
1882
            'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'],
1883
            'argumentCount' => '3',
1884
        ],
1885
        'NORMSDIST' => [
1886
            'category' => Category::CATEGORY_STATISTICAL,
1887
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'cumulative'],
1888
            'argumentCount' => '1',
1889
        ],
1890
        'NORM.S.DIST' => [
1891
            'category' => Category::CATEGORY_STATISTICAL,
1892
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'distribution'],
1893
            'argumentCount' => '1,2',
1894
        ],
1895
        'NORMSINV' => [
1896
            'category' => Category::CATEGORY_STATISTICAL,
1897
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'],
1898
            'argumentCount' => '1',
1899
        ],
1900
        'NORM.S.INV' => [
1901
            'category' => Category::CATEGORY_STATISTICAL,
1902
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'],
1903
            'argumentCount' => '1',
1904
        ],
1905
        'NOT' => [
1906
            'category' => Category::CATEGORY_LOGICAL,
1907
            'functionCall' => [Logical\Operations::class, 'NOT'],
1908
            'argumentCount' => '1',
1909
        ],
1910
        'NOW' => [
1911
            'category' => Category::CATEGORY_DATE_AND_TIME,
1912
            'functionCall' => [DateTimeExcel\Current::class, 'now'],
1913
            'argumentCount' => '0',
1914
        ],
1915
        'NPER' => [
1916
            'category' => Category::CATEGORY_FINANCIAL,
1917
            'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'periods'],
1918
            'argumentCount' => '3-5',
1919
        ],
1920
        'NPV' => [
1921
            'category' => Category::CATEGORY_FINANCIAL,
1922
            'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'presentValue'],
1923
            'argumentCount' => '2+',
1924
        ],
1925
        'NUMBERSTRING' => [
1926
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1927
            'functionCall' => [Functions::class, 'DUMMY'],
1928
            'argumentCount' => '?',
1929
        ],
1930
        'NUMBERVALUE' => [
1931
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1932
            'functionCall' => [TextData\Format::class, 'NUMBERVALUE'],
1933
            'argumentCount' => '1+',
1934
        ],
1935
        'OCT2BIN' => [
1936
            'category' => Category::CATEGORY_ENGINEERING,
1937
            'functionCall' => [Engineering\ConvertOctal::class, 'toBinary'],
1938
            'argumentCount' => '1,2',
1939
        ],
1940
        'OCT2DEC' => [
1941
            'category' => Category::CATEGORY_ENGINEERING,
1942
            'functionCall' => [Engineering\ConvertOctal::class, 'toDecimal'],
1943
            'argumentCount' => '1',
1944
        ],
1945
        'OCT2HEX' => [
1946
            'category' => Category::CATEGORY_ENGINEERING,
1947
            'functionCall' => [Engineering\ConvertOctal::class, 'toHex'],
1948
            'argumentCount' => '1,2',
1949
        ],
1950
        'ODD' => [
1951
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1952
            'functionCall' => [MathTrig\Round::class, 'odd'],
1953
            'argumentCount' => '1',
1954
        ],
1955
        'ODDFPRICE' => [
1956
            'category' => Category::CATEGORY_FINANCIAL,
1957
            'functionCall' => [Functions::class, 'DUMMY'],
1958
            'argumentCount' => '8,9',
1959
        ],
1960
        'ODDFYIELD' => [
1961
            'category' => Category::CATEGORY_FINANCIAL,
1962
            'functionCall' => [Functions::class, 'DUMMY'],
1963
            'argumentCount' => '8,9',
1964
        ],
1965
        'ODDLPRICE' => [
1966
            'category' => Category::CATEGORY_FINANCIAL,
1967
            'functionCall' => [Functions::class, 'DUMMY'],
1968
            'argumentCount' => '7,8',
1969
        ],
1970
        'ODDLYIELD' => [
1971
            'category' => Category::CATEGORY_FINANCIAL,
1972
            'functionCall' => [Functions::class, 'DUMMY'],
1973
            'argumentCount' => '7,8',
1974
        ],
1975
        'OFFSET' => [
1976
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1977
            'functionCall' => [LookupRef\Offset::class, 'OFFSET'],
1978
            'argumentCount' => '3-5',
1979
            'passCellReference' => true,
1980
            'passByReference' => [true],
1981
        ],
1982
        'OR' => [
1983
            'category' => Category::CATEGORY_LOGICAL,
1984
            'functionCall' => [Logical\Operations::class, 'logicalOr'],
1985
            'argumentCount' => '1+',
1986
        ],
1987
        'PDURATION' => [
1988
            'category' => Category::CATEGORY_FINANCIAL,
1989
            'functionCall' => [Financial\CashFlow\Single::class, 'periods'],
1990
            'argumentCount' => '3',
1991
        ],
1992
        'PEARSON' => [
1993
            'category' => Category::CATEGORY_STATISTICAL,
1994
            'functionCall' => [Statistical\Trends::class, 'CORREL'],
1995
            'argumentCount' => '2',
1996
        ],
1997
        'PERCENTILE' => [
1998
            'category' => Category::CATEGORY_STATISTICAL,
1999
            'functionCall' => [Statistical\Percentiles::class, 'PERCENTILE'],
2000
            'argumentCount' => '2',
2001
        ],
2002
        'PERCENTILE.EXC' => [
2003
            'category' => Category::CATEGORY_STATISTICAL,
2004
            'functionCall' => [Functions::class, 'DUMMY'],
2005
            'argumentCount' => '2',
2006
        ],
2007
        'PERCENTILE.INC' => [
2008
            'category' => Category::CATEGORY_STATISTICAL,
2009
            'functionCall' => [Statistical\Percentiles::class, 'PERCENTILE'],
2010
            'argumentCount' => '2',
2011
        ],
2012
        'PERCENTRANK' => [
2013
            'category' => Category::CATEGORY_STATISTICAL,
2014
            'functionCall' => [Statistical\Percentiles::class, 'PERCENTRANK'],
2015
            'argumentCount' => '2,3',
2016
        ],
2017
        'PERCENTRANK.EXC' => [
2018
            'category' => Category::CATEGORY_STATISTICAL,
2019
            'functionCall' => [Functions::class, 'DUMMY'],
2020
            'argumentCount' => '2,3',
2021
        ],
2022
        'PERCENTRANK.INC' => [
2023
            'category' => Category::CATEGORY_STATISTICAL,
2024
            'functionCall' => [Statistical\Percentiles::class, 'PERCENTRANK'],
2025
            'argumentCount' => '2,3',
2026
        ],
2027
        'PERMUT' => [
2028
            'category' => Category::CATEGORY_STATISTICAL,
2029
            'functionCall' => [Statistical\Permutations::class, 'PERMUT'],
2030
            'argumentCount' => '2',
2031
        ],
2032
        'PERMUTATIONA' => [
2033
            'category' => Category::CATEGORY_STATISTICAL,
2034
            'functionCall' => [Statistical\Permutations::class, 'PERMUTATIONA'],
2035
            'argumentCount' => '2',
2036
        ],
2037
        'PHONETIC' => [
2038
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2039
            'functionCall' => [Functions::class, 'DUMMY'],
2040
            'argumentCount' => '1',
2041
        ],
2042
        'PHI' => [
2043
            'category' => Category::CATEGORY_STATISTICAL,
2044
            'functionCall' => [Functions::class, 'DUMMY'],
2045
            'argumentCount' => '1',
2046
        ],
2047
        'PI' => [
2048
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2049
            'functionCall' => 'pi',
2050
            'argumentCount' => '0',
2051
        ],
2052
        'PMT' => [
2053
            'category' => Category::CATEGORY_FINANCIAL,
2054
            'functionCall' => [Financial\CashFlow\Constant\Periodic\Payments::class, 'annuity'],
2055
            'argumentCount' => '3-5',
2056
        ],
2057
        'POISSON' => [
2058
            'category' => Category::CATEGORY_STATISTICAL,
2059
            'functionCall' => [Statistical\Distributions\Poisson::class, 'distribution'],
2060
            'argumentCount' => '3',
2061
        ],
2062
        'POISSON.DIST' => [
2063
            'category' => Category::CATEGORY_STATISTICAL,
2064
            'functionCall' => [Statistical\Distributions\Poisson::class, 'distribution'],
2065
            'argumentCount' => '3',
2066
        ],
2067
        'POWER' => [
2068
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2069
            'functionCall' => [MathTrig\Operations::class, 'power'],
2070
            'argumentCount' => '2',
2071
        ],
2072
        'PPMT' => [
2073
            'category' => Category::CATEGORY_FINANCIAL,
2074
            'functionCall' => [Financial\CashFlow\Constant\Periodic\Payments::class, 'interestPayment'],
2075
            'argumentCount' => '4-6',
2076
        ],
2077
        'PRICE' => [
2078
            'category' => Category::CATEGORY_FINANCIAL,
2079
            'functionCall' => [Financial\Securities\Price::class, 'price'],
2080
            'argumentCount' => '6,7',
2081
        ],
2082
        'PRICEDISC' => [
2083
            'category' => Category::CATEGORY_FINANCIAL,
2084
            'functionCall' => [Financial\Securities\Price::class, 'priceDiscounted'],
2085
            'argumentCount' => '4,5',
2086
        ],
2087
        'PRICEMAT' => [
2088
            'category' => Category::CATEGORY_FINANCIAL,
2089
            'functionCall' => [Financial\Securities\Price::class, 'priceAtMaturity'],
2090
            'argumentCount' => '5,6',
2091
        ],
2092
        'PROB' => [
2093
            'category' => Category::CATEGORY_STATISTICAL,
2094
            'functionCall' => [Functions::class, 'DUMMY'],
2095
            'argumentCount' => '3,4',
2096
        ],
2097
        'PRODUCT' => [
2098
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2099
            'functionCall' => [MathTrig\Operations::class, 'product'],
2100
            'argumentCount' => '1+',
2101
        ],
2102
        'PROPER' => [
2103
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2104
            'functionCall' => [TextData\CaseConvert::class, 'proper'],
2105
            'argumentCount' => '1',
2106
        ],
2107
        'PV' => [
2108
            'category' => Category::CATEGORY_FINANCIAL,
2109
            'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'presentValue'],
2110
            'argumentCount' => '3-5',
2111
        ],
2112
        'QUARTILE' => [
2113
            'category' => Category::CATEGORY_STATISTICAL,
2114
            'functionCall' => [Statistical\Percentiles::class, 'QUARTILE'],
2115
            'argumentCount' => '2',
2116
        ],
2117
        'QUARTILE.EXC' => [
2118
            'category' => Category::CATEGORY_STATISTICAL,
2119
            'functionCall' => [Functions::class, 'DUMMY'],
2120
            'argumentCount' => '2',
2121
        ],
2122
        'QUARTILE.INC' => [
2123
            'category' => Category::CATEGORY_STATISTICAL,
2124
            'functionCall' => [Statistical\Percentiles::class, 'QUARTILE'],
2125
            'argumentCount' => '2',
2126
        ],
2127
        'QUOTIENT' => [
2128
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2129
            'functionCall' => [MathTrig\Operations::class, 'quotient'],
2130
            'argumentCount' => '2',
2131
        ],
2132
        'RADIANS' => [
2133
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2134
            'functionCall' => [MathTrig\Angle::class, 'toRadians'],
2135
            'argumentCount' => '1',
2136
        ],
2137
        'RAND' => [
2138
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2139
            'functionCall' => [MathTrig\Random::class, 'rand'],
2140
            'argumentCount' => '0',
2141
        ],
2142
        'RANDARRAY' => [
2143
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2144
            'functionCall' => [MathTrig\Random::class, 'randArray'],
2145
            'argumentCount' => '0-5',
2146
        ],
2147
        'RANDBETWEEN' => [
2148
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2149
            'functionCall' => [MathTrig\Random::class, 'randBetween'],
2150
            'argumentCount' => '2',
2151
        ],
2152
        'RANK' => [
2153
            'category' => Category::CATEGORY_STATISTICAL,
2154
            'functionCall' => [Statistical\Percentiles::class, 'RANK'],
2155
            'argumentCount' => '2,3',
2156
        ],
2157
        'RANK.AVG' => [
2158
            'category' => Category::CATEGORY_STATISTICAL,
2159
            'functionCall' => [Functions::class, 'DUMMY'],
2160
            'argumentCount' => '2,3',
2161
        ],
2162
        'RANK.EQ' => [
2163
            'category' => Category::CATEGORY_STATISTICAL,
2164
            'functionCall' => [Statistical\Percentiles::class, 'RANK'],
2165
            'argumentCount' => '2,3',
2166
        ],
2167
        'RATE' => [
2168
            'category' => Category::CATEGORY_FINANCIAL,
2169
            'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'rate'],
2170
            'argumentCount' => '3-6',
2171
        ],
2172
        'RECEIVED' => [
2173
            'category' => Category::CATEGORY_FINANCIAL,
2174
            'functionCall' => [Financial\Securities\Price::class, 'received'],
2175
            'argumentCount' => '4-5',
2176
        ],
2177
        'REDUCE' => [
2178
            'category' => Category::CATEGORY_LOGICAL,
2179
            'functionCall' => [Functions::class, 'DUMMY'],
2180
            'argumentCount' => '*',
2181
        ],
2182
        'REPLACE' => [
2183
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2184
            'functionCall' => [TextData\Replace::class, 'replace'],
2185
            'argumentCount' => '4',
2186
        ],
2187
        'REPLACEB' => [
2188
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2189
            'functionCall' => [TextData\Replace::class, 'replace'],
2190
            'argumentCount' => '4',
2191
        ],
2192
        'REPT' => [
2193
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2194
            'functionCall' => [TextData\Concatenate::class, 'builtinREPT'],
2195
            'argumentCount' => '2',
2196
        ],
2197
        'RIGHT' => [
2198
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2199
            'functionCall' => [TextData\Extract::class, 'right'],
2200
            'argumentCount' => '1,2',
2201
        ],
2202
        'RIGHTB' => [
2203
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2204
            'functionCall' => [TextData\Extract::class, 'right'],
2205
            'argumentCount' => '1,2',
2206
        ],
2207
        'ROMAN' => [
2208
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2209
            'functionCall' => [MathTrig\Roman::class, 'evaluate'],
2210
            'argumentCount' => '1,2',
2211
        ],
2212
        'ROUND' => [
2213
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2214
            'functionCall' => [MathTrig\Round::class, 'round'],
2215
            'argumentCount' => '2',
2216
        ],
2217
        'ROUNDBAHTDOWN' => [
2218
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2219
            'functionCall' => [Functions::class, 'DUMMY'],
2220
            'argumentCount' => '?',
2221
        ],
2222
        'ROUNDBAHTUP' => [
2223
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2224
            'functionCall' => [Functions::class, 'DUMMY'],
2225
            'argumentCount' => '?',
2226
        ],
2227
        'ROUNDDOWN' => [
2228
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2229
            'functionCall' => [MathTrig\Round::class, 'down'],
2230
            'argumentCount' => '2',
2231
        ],
2232
        'ROUNDUP' => [
2233
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2234
            'functionCall' => [MathTrig\Round::class, 'up'],
2235
            'argumentCount' => '2',
2236
        ],
2237
        'ROW' => [
2238
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2239
            'functionCall' => [LookupRef\RowColumnInformation::class, 'ROW'],
2240
            'argumentCount' => '-1',
2241
            'passCellReference' => true,
2242
            'passByReference' => [true],
2243
        ],
2244
        'ROWS' => [
2245
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2246
            'functionCall' => [LookupRef\RowColumnInformation::class, 'ROWS'],
2247
            'argumentCount' => '1',
2248
        ],
2249
        'RRI' => [
2250
            'category' => Category::CATEGORY_FINANCIAL,
2251
            'functionCall' => [Financial\CashFlow\Single::class, 'interestRate'],
2252
            'argumentCount' => '3',
2253
        ],
2254
        'RSQ' => [
2255
            'category' => Category::CATEGORY_STATISTICAL,
2256
            'functionCall' => [Statistical\Trends::class, 'RSQ'],
2257
            'argumentCount' => '2',
2258
        ],
2259
        'RTD' => [
2260
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2261
            'functionCall' => [Functions::class, 'DUMMY'],
2262
            'argumentCount' => '1+',
2263
        ],
2264
        'SEARCH' => [
2265
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2266
            'functionCall' => [TextData\Search::class, 'insensitive'],
2267
            'argumentCount' => '2,3',
2268
        ],
2269
        'SCAN' => [
2270
            'category' => Category::CATEGORY_LOGICAL,
2271
            'functionCall' => [Functions::class, 'DUMMY'],
2272
            'argumentCount' => '*',
2273
        ],
2274
        'SEARCHB' => [
2275
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2276
            'functionCall' => [TextData\Search::class, 'insensitive'],
2277
            'argumentCount' => '2,3',
2278
        ],
2279
        'SEC' => [
2280
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2281
            'functionCall' => [MathTrig\Trig\Secant::class, 'sec'],
2282
            'argumentCount' => '1',
2283
        ],
2284
        'SECH' => [
2285
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2286
            'functionCall' => [MathTrig\Trig\Secant::class, 'sech'],
2287
            'argumentCount' => '1',
2288
        ],
2289
        'SECOND' => [
2290
            'category' => Category::CATEGORY_DATE_AND_TIME,
2291
            'functionCall' => [DateTimeExcel\TimeParts::class, 'second'],
2292
            'argumentCount' => '1',
2293
        ],
2294
        'SEQUENCE' => [
2295
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2296
            'functionCall' => [MathTrig\MatrixFunctions::class, 'sequence'],
2297
            'argumentCount' => '1-4',
2298
        ],
2299
        'SERIESSUM' => [
2300
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2301
            'functionCall' => [MathTrig\SeriesSum::class, 'evaluate'],
2302
            'argumentCount' => '4',
2303
        ],
2304
        'SHEET' => [
2305
            'category' => Category::CATEGORY_INFORMATION,
2306
            'functionCall' => [Functions::class, 'DUMMY'],
2307
            'argumentCount' => '0,1',
2308
        ],
2309
        'SHEETS' => [
2310
            'category' => Category::CATEGORY_INFORMATION,
2311
            'functionCall' => [Functions::class, 'DUMMY'],
2312
            'argumentCount' => '0,1',
2313
        ],
2314
        'SIGN' => [
2315
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2316
            'functionCall' => [MathTrig\Sign::class, 'evaluate'],
2317
            'argumentCount' => '1',
2318
        ],
2319
        'SIN' => [
2320
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2321
            'functionCall' => [MathTrig\Trig\Sine::class, 'sin'],
2322
            'argumentCount' => '1',
2323
        ],
2324
        'SINGLE' => [
2325
            'category' => Category::CATEGORY_MICROSOFT_INTERNAL,
2326
            'functionCall' => [Internal\ExcelArrayPseudoFunctions::class, 'single'],
2327
            'argumentCount' => '1',
2328
            'passCellReference' => true,
2329
            'passByReference' => [true],
2330
        ],
2331
        'SINH' => [
2332
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2333
            'functionCall' => [MathTrig\Trig\Sine::class, 'sinh'],
2334
            'argumentCount' => '1',
2335
        ],
2336
        'SKEW' => [
2337
            'category' => Category::CATEGORY_STATISTICAL,
2338
            'functionCall' => [Statistical\Deviations::class, 'skew'],
2339
            'argumentCount' => '1+',
2340
        ],
2341
        'SKEW.P' => [
2342
            'category' => Category::CATEGORY_STATISTICAL,
2343
            'functionCall' => [Functions::class, 'DUMMY'],
2344
            'argumentCount' => '1+',
2345
        ],
2346
        'SLN' => [
2347
            'category' => Category::CATEGORY_FINANCIAL,
2348
            'functionCall' => [Financial\Depreciation::class, 'SLN'],
2349
            'argumentCount' => '3',
2350
        ],
2351
        'SLOPE' => [
2352
            'category' => Category::CATEGORY_STATISTICAL,
2353
            'functionCall' => [Statistical\Trends::class, 'SLOPE'],
2354
            'argumentCount' => '2',
2355
        ],
2356
        'SMALL' => [
2357
            'category' => Category::CATEGORY_STATISTICAL,
2358
            'functionCall' => [Statistical\Size::class, 'small'],
2359
            'argumentCount' => '2',
2360
        ],
2361
        'SORT' => [
2362
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2363
            'functionCall' => [LookupRef\Sort::class, 'sort'],
2364
            'argumentCount' => '1-4',
2365
        ],
2366
        'SORTBY' => [
2367
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2368
            'functionCall' => [LookupRef\Sort::class, 'sortBy'],
2369
            'argumentCount' => '2+',
2370
        ],
2371
        'SQRT' => [
2372
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2373
            'functionCall' => [MathTrig\Sqrt::class, 'sqrt'],
2374
            'argumentCount' => '1',
2375
        ],
2376
        'SQRTPI' => [
2377
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2378
            'functionCall' => [MathTrig\Sqrt::class, 'pi'],
2379
            'argumentCount' => '1',
2380
        ],
2381
        'STANDARDIZE' => [
2382
            'category' => Category::CATEGORY_STATISTICAL,
2383
            'functionCall' => [Statistical\Standardize::class, 'execute'],
2384
            'argumentCount' => '3',
2385
        ],
2386
        'STDEV' => [
2387
            'category' => Category::CATEGORY_STATISTICAL,
2388
            'functionCall' => [Statistical\StandardDeviations::class, 'STDEV'],
2389
            'argumentCount' => '1+',
2390
        ],
2391
        'STDEV.S' => [
2392
            'category' => Category::CATEGORY_STATISTICAL,
2393
            'functionCall' => [Statistical\StandardDeviations::class, 'STDEV'],
2394
            'argumentCount' => '1+',
2395
        ],
2396
        'STDEV.P' => [
2397
            'category' => Category::CATEGORY_STATISTICAL,
2398
            'functionCall' => [Statistical\StandardDeviations::class, 'STDEVP'],
2399
            'argumentCount' => '1+',
2400
        ],
2401
        'STDEVA' => [
2402
            'category' => Category::CATEGORY_STATISTICAL,
2403
            'functionCall' => [Statistical\StandardDeviations::class, 'STDEVA'],
2404
            'argumentCount' => '1+',
2405
        ],
2406
        'STDEVP' => [
2407
            'category' => Category::CATEGORY_STATISTICAL,
2408
            'functionCall' => [Statistical\StandardDeviations::class, 'STDEVP'],
2409
            'argumentCount' => '1+',
2410
        ],
2411
        'STDEVPA' => [
2412
            'category' => Category::CATEGORY_STATISTICAL,
2413
            'functionCall' => [Statistical\StandardDeviations::class, 'STDEVPA'],
2414
            'argumentCount' => '1+',
2415
        ],
2416
        'STEYX' => [
2417
            'category' => Category::CATEGORY_STATISTICAL,
2418
            'functionCall' => [Statistical\Trends::class, 'STEYX'],
2419
            'argumentCount' => '2',
2420
        ],
2421
        'SUBSTITUTE' => [
2422
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2423
            'functionCall' => [TextData\Replace::class, 'substitute'],
2424
            'argumentCount' => '3,4',
2425
        ],
2426
        'SUBTOTAL' => [
2427
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2428
            'functionCall' => [MathTrig\Subtotal::class, 'evaluate'],
2429
            'argumentCount' => '2+',
2430
            'passCellReference' => true,
2431
        ],
2432
        'SUM' => [
2433
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2434
            'functionCall' => [MathTrig\Sum::class, 'sumErroringStrings'],
2435
            'argumentCount' => '1+',
2436
        ],
2437
        'SUMIF' => [
2438
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2439
            'functionCall' => [Statistical\Conditional::class, 'SUMIF'],
2440
            'argumentCount' => '2,3',
2441
        ],
2442
        'SUMIFS' => [
2443
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2444
            'functionCall' => [Statistical\Conditional::class, 'SUMIFS'],
2445
            'argumentCount' => '3+',
2446
        ],
2447
        'SUMPRODUCT' => [
2448
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2449
            'functionCall' => [MathTrig\Sum::class, 'product'],
2450
            'argumentCount' => '1+',
2451
        ],
2452
        'SUMSQ' => [
2453
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2454
            'functionCall' => [MathTrig\SumSquares::class, 'sumSquare'],
2455
            'argumentCount' => '1+',
2456
        ],
2457
        'SUMX2MY2' => [
2458
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2459
            'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredMinusYSquared'],
2460
            'argumentCount' => '2',
2461
        ],
2462
        'SUMX2PY2' => [
2463
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2464
            'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredPlusYSquared'],
2465
            'argumentCount' => '2',
2466
        ],
2467
        'SUMXMY2' => [
2468
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2469
            'functionCall' => [MathTrig\SumSquares::class, 'sumXMinusYSquared'],
2470
            'argumentCount' => '2',
2471
        ],
2472
        'SWITCH' => [
2473
            'category' => Category::CATEGORY_LOGICAL,
2474
            'functionCall' => [Logical\Conditional::class, 'statementSwitch'],
2475
            'argumentCount' => '3+',
2476
        ],
2477
        'SYD' => [
2478
            'category' => Category::CATEGORY_FINANCIAL,
2479
            'functionCall' => [Financial\Depreciation::class, 'SYD'],
2480
            'argumentCount' => '4',
2481
        ],
2482
        'T' => [
2483
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2484
            'functionCall' => [TextData\Text::class, 'test'],
2485
            'argumentCount' => '1',
2486
        ],
2487
        'TAKE' => [
2488
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2489
            'functionCall' => [Functions::class, 'DUMMY'],
2490
            'argumentCount' => '2-3',
2491
        ],
2492
        'TAN' => [
2493
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2494
            'functionCall' => [MathTrig\Trig\Tangent::class, 'tan'],
2495
            'argumentCount' => '1',
2496
        ],
2497
        'TANH' => [
2498
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2499
            'functionCall' => [MathTrig\Trig\Tangent::class, 'tanh'],
2500
            'argumentCount' => '1',
2501
        ],
2502
        'TBILLEQ' => [
2503
            'category' => Category::CATEGORY_FINANCIAL,
2504
            'functionCall' => [Financial\TreasuryBill::class, 'bondEquivalentYield'],
2505
            'argumentCount' => '3',
2506
        ],
2507
        'TBILLPRICE' => [
2508
            'category' => Category::CATEGORY_FINANCIAL,
2509
            'functionCall' => [Financial\TreasuryBill::class, 'price'],
2510
            'argumentCount' => '3',
2511
        ],
2512
        'TBILLYIELD' => [
2513
            'category' => Category::CATEGORY_FINANCIAL,
2514
            'functionCall' => [Financial\TreasuryBill::class, 'yield'],
2515
            'argumentCount' => '3',
2516
        ],
2517
        'TDIST' => [
2518
            'category' => Category::CATEGORY_STATISTICAL,
2519
            'functionCall' => [Statistical\Distributions\StudentT::class, 'distribution'],
2520
            'argumentCount' => '3',
2521
        ],
2522
        'T.DIST' => [
2523
            'category' => Category::CATEGORY_STATISTICAL,
2524
            'functionCall' => [Functions::class, 'DUMMY'],
2525
            'argumentCount' => '3',
2526
        ],
2527
        'T.DIST.2T' => [
2528
            'category' => Category::CATEGORY_STATISTICAL,
2529
            'functionCall' => [Functions::class, 'DUMMY'],
2530
            'argumentCount' => '2',
2531
        ],
2532
        'T.DIST.RT' => [
2533
            'category' => Category::CATEGORY_STATISTICAL,
2534
            'functionCall' => [Functions::class, 'DUMMY'],
2535
            'argumentCount' => '2',
2536
        ],
2537
        'TEXT' => [
2538
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2539
            'functionCall' => [TextData\Format::class, 'TEXTFORMAT'],
2540
            'argumentCount' => '2',
2541
        ],
2542
        'TEXTAFTER' => [
2543
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2544
            'functionCall' => [TextData\Extract::class, 'after'],
2545
            'argumentCount' => '2-6',
2546
        ],
2547
        'TEXTBEFORE' => [
2548
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2549
            'functionCall' => [TextData\Extract::class, 'before'],
2550
            'argumentCount' => '2-6',
2551
        ],
2552
        'TEXTJOIN' => [
2553
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2554
            'functionCall' => [TextData\Concatenate::class, 'TEXTJOIN'],
2555
            'argumentCount' => '3+',
2556
        ],
2557
        'TEXTSPLIT' => [
2558
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2559
            'functionCall' => [TextData\Text::class, 'split'],
2560
            'argumentCount' => '2-6',
2561
        ],
2562
        'THAIDAYOFWEEK' => [
2563
            'category' => Category::CATEGORY_DATE_AND_TIME,
2564
            'functionCall' => [Functions::class, 'DUMMY'],
2565
            'argumentCount' => '?',
2566
        ],
2567
        'THAIDIGIT' => [
2568
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2569
            'functionCall' => [Functions::class, 'DUMMY'],
2570
            'argumentCount' => '?',
2571
        ],
2572
        'THAIMONTHOFYEAR' => [
2573
            'category' => Category::CATEGORY_DATE_AND_TIME,
2574
            'functionCall' => [Functions::class, 'DUMMY'],
2575
            'argumentCount' => '?',
2576
        ],
2577
        'THAINUMSOUND' => [
2578
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2579
            'functionCall' => [Functions::class, 'DUMMY'],
2580
            'argumentCount' => '?',
2581
        ],
2582
        'THAINUMSTRING' => [
2583
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2584
            'functionCall' => [Functions::class, 'DUMMY'],
2585
            'argumentCount' => '?',
2586
        ],
2587
        'THAISTRINGLENGTH' => [
2588
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2589
            'functionCall' => [Functions::class, 'DUMMY'],
2590
            'argumentCount' => '?',
2591
        ],
2592
        'THAIYEAR' => [
2593
            'category' => Category::CATEGORY_DATE_AND_TIME,
2594
            'functionCall' => [Functions::class, 'DUMMY'],
2595
            'argumentCount' => '?',
2596
        ],
2597
        'TIME' => [
2598
            'category' => Category::CATEGORY_DATE_AND_TIME,
2599
            'functionCall' => [DateTimeExcel\Time::class, 'fromHMS'],
2600
            'argumentCount' => '3',
2601
        ],
2602
        'TIMEVALUE' => [
2603
            'category' => Category::CATEGORY_DATE_AND_TIME,
2604
            'functionCall' => [DateTimeExcel\TimeValue::class, 'fromString'],
2605
            'argumentCount' => '1',
2606
        ],
2607
        'TINV' => [
2608
            'category' => Category::CATEGORY_STATISTICAL,
2609
            'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
2610
            'argumentCount' => '2',
2611
        ],
2612
        'T.INV' => [
2613
            'category' => Category::CATEGORY_STATISTICAL,
2614
            'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
2615
            'argumentCount' => '2',
2616
        ],
2617
        'T.INV.2T' => [
2618
            'category' => Category::CATEGORY_STATISTICAL,
2619
            'functionCall' => [Functions::class, 'DUMMY'],
2620
            'argumentCount' => '2',
2621
        ],
2622
        'TODAY' => [
2623
            'category' => Category::CATEGORY_DATE_AND_TIME,
2624
            'functionCall' => [DateTimeExcel\Current::class, 'today'],
2625
            'argumentCount' => '0',
2626
        ],
2627
        'TOCOL' => [
2628
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2629
            'functionCall' => [Functions::class, 'DUMMY'],
2630
            'argumentCount' => '1-3',
2631
        ],
2632
        'TOROW' => [
2633
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2634
            'functionCall' => [Functions::class, 'DUMMY'],
2635
            'argumentCount' => '1-3',
2636
        ],
2637
        'TRANSPOSE' => [
2638
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2639
            'functionCall' => [LookupRef\Matrix::class, 'transpose'],
2640
            'argumentCount' => '1',
2641
        ],
2642
        'TREND' => [
2643
            'category' => Category::CATEGORY_STATISTICAL,
2644
            'functionCall' => [Statistical\Trends::class, 'TREND'],
2645
            'argumentCount' => '1-4',
2646
        ],
2647
        'TRIM' => [
2648
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2649
            'functionCall' => [TextData\Trim::class, 'spaces'],
2650
            'argumentCount' => '1',
2651
        ],
2652
        'TRIMMEAN' => [
2653
            'category' => Category::CATEGORY_STATISTICAL,
2654
            'functionCall' => [Statistical\Averages\Mean::class, 'trim'],
2655
            'argumentCount' => '2',
2656
        ],
2657
        'TRUE' => [
2658
            'category' => Category::CATEGORY_LOGICAL,
2659
            'functionCall' => [Logical\Boolean::class, 'TRUE'],
2660
            'argumentCount' => '0',
2661
        ],
2662
        'TRUNC' => [
2663
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2664
            'functionCall' => [MathTrig\Trunc::class, 'evaluate'],
2665
            'argumentCount' => '1,2',
2666
        ],
2667
        'TTEST' => [
2668
            'category' => Category::CATEGORY_STATISTICAL,
2669
            'functionCall' => [Functions::class, 'DUMMY'],
2670
            'argumentCount' => '4',
2671
        ],
2672
        'T.TEST' => [
2673
            'category' => Category::CATEGORY_STATISTICAL,
2674
            'functionCall' => [Functions::class, 'DUMMY'],
2675
            'argumentCount' => '4',
2676
        ],
2677
        'TYPE' => [
2678
            'category' => Category::CATEGORY_INFORMATION,
2679
            'functionCall' => [Information\Value::class, 'type'],
2680
            'argumentCount' => '1',
2681
        ],
2682
        'UNICHAR' => [
2683
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2684
            'functionCall' => [TextData\CharacterConvert::class, 'character'],
2685
            'argumentCount' => '1',
2686
        ],
2687
        'UNICODE' => [
2688
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2689
            'functionCall' => [TextData\CharacterConvert::class, 'code'],
2690
            'argumentCount' => '1',
2691
        ],
2692
        'UNIQUE' => [
2693
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2694
            'functionCall' => [LookupRef\Unique::class, 'unique'],
2695
            'argumentCount' => '1+',
2696
        ],
2697
        'UPPER' => [
2698
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2699
            'functionCall' => [TextData\CaseConvert::class, 'upper'],
2700
            'argumentCount' => '1',
2701
        ],
2702
        'USDOLLAR' => [
2703
            'category' => Category::CATEGORY_FINANCIAL,
2704
            'functionCall' => [Financial\Dollar::class, 'format'],
2705
            'argumentCount' => '2',
2706
        ],
2707
        'VALUE' => [
2708
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2709
            'functionCall' => [TextData\Format::class, 'VALUE'],
2710
            'argumentCount' => '1',
2711
        ],
2712
        'VALUETOTEXT' => [
2713
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2714
            'functionCall' => [TextData\Format::class, 'valueToText'],
2715
            'argumentCount' => '1,2',
2716
        ],
2717
        'VAR' => [
2718
            'category' => Category::CATEGORY_STATISTICAL,
2719
            'functionCall' => [Statistical\Variances::class, 'VAR'],
2720
            'argumentCount' => '1+',
2721
        ],
2722
        'VAR.P' => [
2723
            'category' => Category::CATEGORY_STATISTICAL,
2724
            'functionCall' => [Statistical\Variances::class, 'VARP'],
2725
            'argumentCount' => '1+',
2726
        ],
2727
        'VAR.S' => [
2728
            'category' => Category::CATEGORY_STATISTICAL,
2729
            'functionCall' => [Statistical\Variances::class, 'VAR'],
2730
            'argumentCount' => '1+',
2731
        ],
2732
        'VARA' => [
2733
            'category' => Category::CATEGORY_STATISTICAL,
2734
            'functionCall' => [Statistical\Variances::class, 'VARA'],
2735
            'argumentCount' => '1+',
2736
        ],
2737
        'VARP' => [
2738
            'category' => Category::CATEGORY_STATISTICAL,
2739
            'functionCall' => [Statistical\Variances::class, 'VARP'],
2740
            'argumentCount' => '1+',
2741
        ],
2742
        'VARPA' => [
2743
            'category' => Category::CATEGORY_STATISTICAL,
2744
            'functionCall' => [Statistical\Variances::class, 'VARPA'],
2745
            'argumentCount' => '1+',
2746
        ],
2747
        'VDB' => [
2748
            'category' => Category::CATEGORY_FINANCIAL,
2749
            'functionCall' => [Functions::class, 'DUMMY'],
2750
            'argumentCount' => '5-7',
2751
        ],
2752
        'VLOOKUP' => [
2753
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2754
            'functionCall' => [LookupRef\VLookup::class, 'lookup'],
2755
            'argumentCount' => '3,4',
2756
        ],
2757
        'VSTACK' => [
2758
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2759
            'functionCall' => [Functions::class, 'DUMMY'],
2760
            'argumentCount' => '1+',
2761
        ],
2762
        'WEBSERVICE' => [
2763
            'category' => Category::CATEGORY_WEB,
2764
            'functionCall' => [Web\Service::class, 'webService'],
2765
            'argumentCount' => '1',
2766
        ],
2767
        'WEEKDAY' => [
2768
            'category' => Category::CATEGORY_DATE_AND_TIME,
2769
            'functionCall' => [DateTimeExcel\Week::class, 'day'],
2770
            'argumentCount' => '1,2',
2771
        ],
2772
        'WEEKNUM' => [
2773
            'category' => Category::CATEGORY_DATE_AND_TIME,
2774
            'functionCall' => [DateTimeExcel\Week::class, 'number'],
2775
            'argumentCount' => '1,2',
2776
        ],
2777
        'WEIBULL' => [
2778
            'category' => Category::CATEGORY_STATISTICAL,
2779
            'functionCall' => [Statistical\Distributions\Weibull::class, 'distribution'],
2780
            'argumentCount' => '4',
2781
        ],
2782
        'WEIBULL.DIST' => [
2783
            'category' => Category::CATEGORY_STATISTICAL,
2784
            'functionCall' => [Statistical\Distributions\Weibull::class, 'distribution'],
2785
            'argumentCount' => '4',
2786
        ],
2787
        'WORKDAY' => [
2788
            'category' => Category::CATEGORY_DATE_AND_TIME,
2789
            'functionCall' => [DateTimeExcel\WorkDay::class, 'date'],
2790
            'argumentCount' => '2-3',
2791
        ],
2792
        'WORKDAY.INTL' => [
2793
            'category' => Category::CATEGORY_DATE_AND_TIME,
2794
            'functionCall' => [Functions::class, 'DUMMY'],
2795
            'argumentCount' => '2-4',
2796
        ],
2797
        'WRAPCOLS' => [
2798
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2799
            'functionCall' => [Functions::class, 'DUMMY'],
2800
            'argumentCount' => '2-3',
2801
        ],
2802
        'WRAPROWS' => [
2803
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2804
            'functionCall' => [Functions::class, 'DUMMY'],
2805
            'argumentCount' => '2-3',
2806
        ],
2807
        'XIRR' => [
2808
            'category' => Category::CATEGORY_FINANCIAL,
2809
            'functionCall' => [Financial\CashFlow\Variable\NonPeriodic::class, 'rate'],
2810
            'argumentCount' => '2,3',
2811
        ],
2812
        'XLOOKUP' => [
2813
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2814
            'functionCall' => [Functions::class, 'DUMMY'],
2815
            'argumentCount' => '3-6',
2816
        ],
2817
        'XNPV' => [
2818
            'category' => Category::CATEGORY_FINANCIAL,
2819
            'functionCall' => [Financial\CashFlow\Variable\NonPeriodic::class, 'presentValue'],
2820
            'argumentCount' => '3',
2821
        ],
2822
        'XMATCH' => [
2823
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2824
            'functionCall' => [Functions::class, 'DUMMY'],
2825
            'argumentCount' => '2,3',
2826
        ],
2827
        'XOR' => [
2828
            'category' => Category::CATEGORY_LOGICAL,
2829
            'functionCall' => [Logical\Operations::class, 'logicalXor'],
2830
            'argumentCount' => '1+',
2831
        ],
2832
        'YEAR' => [
2833
            'category' => Category::CATEGORY_DATE_AND_TIME,
2834
            'functionCall' => [DateTimeExcel\DateParts::class, 'year'],
2835
            'argumentCount' => '1',
2836
        ],
2837
        'YEARFRAC' => [
2838
            'category' => Category::CATEGORY_DATE_AND_TIME,
2839
            'functionCall' => [DateTimeExcel\YearFrac::class, 'fraction'],
2840
            'argumentCount' => '2,3',
2841
        ],
2842
        'YIELD' => [
2843
            'category' => Category::CATEGORY_FINANCIAL,
2844
            'functionCall' => [Functions::class, 'DUMMY'],
2845
            'argumentCount' => '6,7',
2846
        ],
2847
        'YIELDDISC' => [
2848
            'category' => Category::CATEGORY_FINANCIAL,
2849
            'functionCall' => [Financial\Securities\Yields::class, 'yieldDiscounted'],
2850
            'argumentCount' => '4,5',
2851
        ],
2852
        'YIELDMAT' => [
2853
            'category' => Category::CATEGORY_FINANCIAL,
2854
            'functionCall' => [Financial\Securities\Yields::class, 'yieldAtMaturity'],
2855
            'argumentCount' => '5,6',
2856
        ],
2857
        'ZTEST' => [
2858
            'category' => Category::CATEGORY_STATISTICAL,
2859
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'],
2860
            'argumentCount' => '2-3',
2861
        ],
2862
        'Z.TEST' => [
2863
            'category' => Category::CATEGORY_STATISTICAL,
2864
            'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'],
2865
            'argumentCount' => '2-3',
2866
        ],
2867
    ];
2868
2869
    /**
2870
     *    Internal functions used for special control purposes.
2871
     */
2872
    private static array $controlFunctions = [
2873
        'MKMATRIX' => [
2874
            'argumentCount' => '*',
2875
            'functionCall' => [Internal\MakeMatrix::class, 'make'],
2876
        ],
2877
        'NAME.ERROR' => [
2878
            'argumentCount' => '*',
2879
            'functionCall' => [ExcelError::class, 'NAME'],
2880
        ],
2881
        'WILDCARDMATCH' => [
2882
            'argumentCount' => '2',
2883
            'functionCall' => [Internal\WildcardMatch::class, 'compare'],
2884
        ],
2885
    ];
2886
2887 10190
    public function __construct(?Spreadsheet $spreadsheet = null)
2888
    {
2889 10190
        $this->spreadsheet = $spreadsheet;
2890 10190
        $this->cyclicReferenceStack = new CyclicReferenceStack();
2891 10190
        $this->debugLog = new Logger($this->cyclicReferenceStack);
2892 10190
        $this->branchPruner = new BranchPruner($this->branchPruningEnabled);
2893 10190
        self::$referenceHelper = ReferenceHelper::getInstance();
2894
    }
2895
2896 1
    private static function loadLocales(): void
2897
    {
2898 1
        $localeFileDirectory = __DIR__ . '/locale/';
2899 1
        $localeFileNames = glob($localeFileDirectory . '*', GLOB_ONLYDIR) ?: [];
2900 1
        foreach ($localeFileNames as $filename) {
2901 1
            $filename = substr($filename, strlen($localeFileDirectory));
2902 1
            if ($filename != 'en') {
2903 1
                self::$validLocaleLanguages[] = $filename;
2904
            }
2905
        }
2906
    }
2907
2908
    /**
2909
     * Get an instance of this class.
2910
     *
2911
     * @param ?Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object,
2912
     *                                    or NULL to create a standalone calculation engine
2913
     */
2914 12756
    public static function getInstance(?Spreadsheet $spreadsheet = null): self
2915
    {
2916 12756
        if ($spreadsheet !== null) {
2917 9003
            $instance = $spreadsheet->getCalculationEngine();
2918 9003
            if (isset($instance)) {
2919 9003
                return $instance;
2920
            }
2921
        }
2922
2923 4610
        if (!self::$instance) {
2924 16
            self::$instance = new self();
2925
        }
2926
2927 4610
        return self::$instance;
2928
    }
2929
2930
    /**
2931
     * Flush the calculation cache for any existing instance of this class
2932
     *        but only if a Calculation instance exists.
2933
     */
2934 201
    public function flushInstance(): void
2935
    {
2936 201
        $this->clearCalculationCache();
2937 201
        $this->branchPruner->clearBranchStore();
2938
    }
2939
2940
    /**
2941
     * Get the Logger for this calculation engine instance.
2942
     */
2943 1017
    public function getDebugLog(): Logger
2944
    {
2945 1017
        return $this->debugLog;
2946
    }
2947
2948
    /**
2949
     * __clone implementation. Cloning should not be allowed in a Singleton!
2950
     */
2951
    final public function __clone()
2952
    {
2953
        throw new Exception('Cloning the calculation engine is not allowed!');
2954
    }
2955
2956
    /**
2957
     * Return the locale-specific translation of TRUE.
2958
     *
2959
     * @return string locale-specific translation of TRUE
2960
     */
2961 417
    public static function getTRUE(): string
2962
    {
2963 417
        return self::$localeBoolean['TRUE'];
2964
    }
2965
2966
    /**
2967
     * Return the locale-specific translation of FALSE.
2968
     *
2969
     * @return string locale-specific translation of FALSE
2970
     */
2971 401
    public static function getFALSE(): string
2972
    {
2973 401
        return self::$localeBoolean['FALSE'];
2974
    }
2975
2976
    /**
2977
     * Set the Array Return Type (Array or Value of first element in the array).
2978
     *
2979
     * @param string $returnType Array return type
2980
     *
2981
     * @return bool Success or failure
2982
     */
2983 1
    public static function setArrayReturnType(string $returnType): bool
2984
    {
2985
        if (
2986 1
            ($returnType == self::RETURN_ARRAY_AS_VALUE)
2987 1
            || ($returnType == self::RETURN_ARRAY_AS_ERROR)
2988 1
            || ($returnType == self::RETURN_ARRAY_AS_ARRAY)
2989
        ) {
2990 1
            self::$returnArrayAsType = $returnType;
2991
2992 1
            return true;
2993
        }
2994
2995 1
        return false;
2996
    }
2997
2998
    /**
2999
     * Return the Array Return Type (Array or Value of first element in the array).
3000
     *
3001
     * @return string $returnType Array return type
3002
     */
3003 1
    public static function getArrayReturnType(): string
3004
    {
3005 1
        return self::$returnArrayAsType;
3006
    }
3007
3008
    /**
3009
     * Set the Instance Array Return Type (Array or Value of first element in the array).
3010
     *
3011
     * @param string $returnType Array return type
3012
     *
3013
     * @return bool Success or failure
3014
     */
3015 63
    public function setInstanceArrayReturnType(string $returnType): bool
3016
    {
3017
        if (
3018 63
            ($returnType == self::RETURN_ARRAY_AS_VALUE)
3019 63
            || ($returnType == self::RETURN_ARRAY_AS_ERROR)
3020 63
            || ($returnType == self::RETURN_ARRAY_AS_ARRAY)
3021
        ) {
3022 63
            $this->instanceArrayReturnType = $returnType;
3023
3024 63
            return true;
3025
        }
3026
3027
        return false;
3028
    }
3029
3030
    /**
3031
     * Return the Array Return Type (Array or Value of first element in the array).
3032
     *
3033
     * @return string $returnType Array return type for instance if non-null, otherwise static property
3034
     */
3035 7619
    public function getInstanceArrayReturnType(): string
3036
    {
3037 7619
        return $this->instanceArrayReturnType ?? self::$returnArrayAsType;
3038
    }
3039
3040
    /**
3041
     * Is calculation caching enabled?
3042
     */
3043 174
    public function getCalculationCacheEnabled(): bool
3044
    {
3045 174
        return $this->calculationCacheEnabled;
3046
    }
3047
3048
    /**
3049
     * Enable/disable calculation cache.
3050
     */
3051
    public function setCalculationCacheEnabled(bool $calculationCacheEnabled): void
3052
    {
3053
        $this->calculationCacheEnabled = $calculationCacheEnabled;
3054
        $this->clearCalculationCache();
3055
    }
3056
3057
    /**
3058
     * Enable calculation cache.
3059
     */
3060
    public function enableCalculationCache(): void
3061
    {
3062
        $this->setCalculationCacheEnabled(true);
3063
    }
3064
3065
    /**
3066
     * Disable calculation cache.
3067
     */
3068
    public function disableCalculationCache(): void
3069
    {
3070
        $this->setCalculationCacheEnabled(false);
3071
    }
3072
3073
    /**
3074
     * Clear calculation cache.
3075
     */
3076 203
    public function clearCalculationCache(): void
3077
    {
3078 203
        $this->calculationCache = [];
3079
    }
3080
3081
    /**
3082
     * Clear calculation cache for a specified worksheet.
3083
     */
3084 125
    public function clearCalculationCacheForWorksheet(string $worksheetName): void
3085
    {
3086 125
        if (isset($this->calculationCache[$worksheetName])) {
3087
            unset($this->calculationCache[$worksheetName]);
3088
        }
3089
    }
3090
3091
    /**
3092
     * Rename calculation cache for a specified worksheet.
3093
     */
3094 10184
    public function renameCalculationCacheForWorksheet(string $fromWorksheetName, string $toWorksheetName): void
3095
    {
3096 10184
        if (isset($this->calculationCache[$fromWorksheetName])) {
3097
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
3098
            unset($this->calculationCache[$fromWorksheetName]);
3099
        }
3100
    }
3101
3102
    /**
3103
     * Enable/disable calculation cache.
3104
     */
3105 7851
    public function setBranchPruningEnabled(mixed $enabled): void
3106
    {
3107 7851
        $this->branchPruningEnabled = $enabled;
3108 7851
        $this->branchPruner = new BranchPruner($this->branchPruningEnabled);
3109
    }
3110
3111
    public function enableBranchPruning(): void
3112
    {
3113
        $this->setBranchPruningEnabled(true);
3114
    }
3115
3116 7851
    public function disableBranchPruning(): void
3117
    {
3118 7851
        $this->setBranchPruningEnabled(false);
3119
    }
3120
3121
    /**
3122
     * Get the currently defined locale code.
3123
     */
3124 766
    public function getLocale(): string
3125
    {
3126 766
        return self::$localeLanguage;
3127
    }
3128
3129 117
    private function getLocaleFile(string $localeDir, string $locale, string $language, string $file): string
3130
    {
3131 117
        $localeFileName = $localeDir . str_replace('_', DIRECTORY_SEPARATOR, $locale)
3132 117
            . DIRECTORY_SEPARATOR . $file;
3133 117
        if (!file_exists($localeFileName)) {
3134
            //    If there isn't a locale specific file, look for a language specific file
3135 29
            $localeFileName = $localeDir . $language . DIRECTORY_SEPARATOR . $file;
3136 29
            if (!file_exists($localeFileName)) {
3137 3
                throw new Exception('Locale file not found');
3138
            }
3139
        }
3140
3141 114
        return $localeFileName;
3142
    }
3143
3144
    /**
3145
     * Set the locale code.
3146
     *
3147
     * @param string $locale The locale to use for formula translation, eg: 'en_us'
3148
     */
3149 766
    public function setLocale(string $locale): bool
3150
    {
3151
        //    Identify our locale and language
3152 766
        $language = $locale = strtolower($locale);
3153 766
        if (str_contains($locale, '_')) {
3154 766
            [$language] = explode('_', $locale);
3155
        }
3156 766
        if (count(self::$validLocaleLanguages) == 1) {
3157 1
            self::loadLocales();
3158
        }
3159
3160
        //    Test whether we have any language data for this language (any locale)
3161 766
        if (in_array($language, self::$validLocaleLanguages, true)) {
3162
            //    initialise language/locale settings
3163 766
            self::$localeFunctions = [];
3164 766
            self::$localeArgumentSeparator = ',';
3165 766
            self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL'];
3166
3167
            //    Default is US English, if user isn't requesting US english, then read the necessary data from the locale files
3168 766
            if ($locale !== 'en_us') {
3169 117
                $localeDir = implode(DIRECTORY_SEPARATOR, [__DIR__, 'locale', null]);
3170
3171
                //    Search for a file with a list of function names for locale
3172
                try {
3173 117
                    $functionNamesFile = $this->getLocaleFile($localeDir, $locale, $language, 'functions');
3174 3
                } catch (Exception $e) {
3175 3
                    return false;
3176
                }
3177
3178
                //    Retrieve the list of locale or language specific function names
3179 114
                $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
3180 114
                foreach ($localeFunctions as $localeFunction) {
3181 114
                    [$localeFunction] = explode('##', $localeFunction); //    Strip out comments
3182 114
                    if (str_contains($localeFunction, '=')) {
3183 114
                        [$fName, $lfName] = array_map('trim', explode('=', $localeFunction));
3184 114
                        if ((str_starts_with($fName, '*') || isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) {
3185 114
                            self::$localeFunctions[$fName] = $lfName;
3186
                        }
3187
                    }
3188
                }
3189
                //    Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions
3190 114
                if (isset(self::$localeFunctions['TRUE'])) {
3191 114
                    self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE'];
3192
                }
3193 114
                if (isset(self::$localeFunctions['FALSE'])) {
3194 114
                    self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE'];
3195
                }
3196
3197
                try {
3198 114
                    $configFile = $this->getLocaleFile($localeDir, $locale, $language, 'config');
3199
                } catch (Exception) {
3200
                    return false;
3201
                }
3202
3203 114
                $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
3204 114
                foreach ($localeSettings as $localeSetting) {
3205 114
                    [$localeSetting] = explode('##', $localeSetting); //    Strip out comments
3206 114
                    if (str_contains($localeSetting, '=')) {
3207 114
                        [$settingName, $settingValue] = array_map('trim', explode('=', $localeSetting));
3208 114
                        $settingName = strtoupper($settingName);
3209 114
                        if ($settingValue !== '') {
3210
                            switch ($settingName) {
3211 114
                                case 'ARGUMENTSEPARATOR':
3212 114
                                    self::$localeArgumentSeparator = $settingValue;
3213
3214 114
                                    break;
3215
                            }
3216
                        }
3217
                    }
3218
                }
3219
            }
3220
3221 766
            self::$functionReplaceFromExcel = self::$functionReplaceToExcel
3222 766
            = self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
3223 766
            self::$localeLanguage = $locale;
3224
3225 766
            return true;
3226
        }
3227
3228 3
        return false;
3229
    }
3230
3231 42
    public static function translateSeparator(
3232
        string $fromSeparator,
3233
        string $toSeparator,
3234
        string $formula,
3235
        int &$inBracesLevel,
3236
        string $openBrace = self::FORMULA_OPEN_FUNCTION_BRACE,
3237
        string $closeBrace = self::FORMULA_CLOSE_FUNCTION_BRACE
3238
    ): string {
3239 42
        $strlen = mb_strlen($formula);
3240 42
        for ($i = 0; $i < $strlen; ++$i) {
3241 42
            $chr = mb_substr($formula, $i, 1);
3242
            switch ($chr) {
3243 42
                case $openBrace:
3244 38
                    ++$inBracesLevel;
3245
3246 38
                    break;
3247 42
                case $closeBrace:
3248 38
                    --$inBracesLevel;
3249
3250 38
                    break;
3251 42
                case $fromSeparator:
3252 23
                    if ($inBracesLevel > 0) {
3253 23
                        $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
3254
                    }
3255
            }
3256
        }
3257
3258 42
        return $formula;
3259
    }
3260
3261 19
    private static function translateFormulaBlock(
3262
        array $from,
3263
        array $to,
3264
        string $formula,
3265
        int &$inFunctionBracesLevel,
3266
        int &$inMatrixBracesLevel,
3267
        string $fromSeparator,
3268
        string $toSeparator
3269
    ): string {
3270
        // Function Names
3271 19
        $formula = (string) preg_replace($from, $to, $formula);
3272
3273
        // Temporarily adjust matrix separators so that they won't be confused with function arguments
3274 19
        $formula = self::translateSeparator(';', '|', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
3275 19
        $formula = self::translateSeparator(',', '!', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
3276
        // Function Argument Separators
3277 19
        $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inFunctionBracesLevel);
3278
        // Restore matrix separators
3279 19
        $formula = self::translateSeparator('|', ';', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
3280 19
        $formula = self::translateSeparator('!', ',', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
3281
3282 19
        return $formula;
3283
    }
3284
3285 19
    private static function translateFormula(array $from, array $to, string $formula, string $fromSeparator, string $toSeparator): string
3286
    {
3287
        // Convert any Excel function names and constant names to the required language;
3288
        //     and adjust function argument separators
3289 19
        if (self::$localeLanguage !== 'en_us') {
3290 19
            $inFunctionBracesLevel = 0;
3291 19
            $inMatrixBracesLevel = 0;
3292
            //    If there is the possibility of separators within a quoted string, then we treat them as literals
3293 19
            if (str_contains($formula, self::FORMULA_STRING_QUOTE)) {
3294
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element
3295
                //       after we've exploded the formula
3296 6
                $temp = explode(self::FORMULA_STRING_QUOTE, $formula);
3297 6
                $notWithinQuotes = false;
3298 6
                foreach ($temp as &$value) {
3299
                    //    Only adjust in alternating array entries
3300 6
                    $notWithinQuotes = $notWithinQuotes === false;
3301 6
                    if ($notWithinQuotes === true) {
3302 6
                        $value = self::translateFormulaBlock($from, $to, $value, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator);
3303
                    }
3304
                }
3305 6
                unset($value);
3306
                //    Then rebuild the formula string
3307 6
                $formula = implode(self::FORMULA_STRING_QUOTE, $temp);
3308
            } else {
3309
                //    If there's no quoted strings, then we do a simple count/replace
3310 13
                $formula = self::translateFormulaBlock($from, $to, $formula, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator);
3311
            }
3312
        }
3313
3314 19
        return $formula;
3315
    }
3316
3317
    private static ?array $functionReplaceFromExcel;
3318
3319
    private static ?array $functionReplaceToLocale;
3320
3321
    public function translateFormulaToLocale(string $formula): string
3322
    {
3323
        $formula = preg_replace(self::CALCULATION_REGEXP_STRIP_XLFN_XLWS, '', $formula) ?? '';
3324
        // Build list of function names and constants for translation
3325
        if (self::$functionReplaceFromExcel === null) {
3326
            self::$functionReplaceFromExcel = [];
3327
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
3328
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/ui';
3329
            }
3330
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
3331 19
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui';
3332
            }
3333 19
        }
3334
3335 19
        if (self::$functionReplaceToLocale === null) {
3336 19
            self::$functionReplaceToLocale = [];
3337 19
            foreach (self::$localeFunctions as $localeFunctionName) {
3338 19
                self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2';
3339
            }
3340 19
            foreach (self::$localeBoolean as $localeBoolean) {
3341 19
                self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2';
3342
            }
3343
        }
3344
3345 19
        return self::translateFormula(
3346 19
            self::$functionReplaceFromExcel,
3347 19
            self::$functionReplaceToLocale,
3348 19
            $formula,
3349
            ',',
3350 19
            self::$localeArgumentSeparator
3351 19
        );
3352
    }
3353
3354
    private static ?array $functionReplaceFromLocale;
3355 19
3356 19
    private static ?array $functionReplaceToExcel;
3357 19
3358 19
    public function translateFormulaToEnglish(string $formula): string
3359 19
    {
3360 19
        if (self::$functionReplaceFromLocale === null) {
3361 19
            self::$functionReplaceFromLocale = [];
3362
            foreach (self::$localeFunctions as $localeFunctionName) {
3363
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/ui';
3364
            }
3365
            foreach (self::$localeBoolean as $excelBoolean) {
3366
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui';
3367
            }
3368
        }
3369
3370
        if (self::$functionReplaceToExcel === null) {
3371
            self::$functionReplaceToExcel = [];
3372
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
3373
                self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2';
3374
            }
3375
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
3376
                self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2';
3377
            }
3378 19
        }
3379
3380 19
        return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ',');
3381 19
    }
3382 19
3383 19
    public static function localeFunc(string $function): string
3384
    {
3385 19
        if (self::$localeLanguage !== 'en_us') {
3386 19
            $functionName = trim($function, '(');
3387
            if (isset(self::$localeFunctions[$functionName])) {
3388
                $brace = ($functionName != $function);
3389
                $function = self::$localeFunctions[$functionName];
3390 19
                if ($brace) {
3391 19
                    $function .= '(';
3392 19
                }
3393 19
            }
3394
        }
3395 19
3396 19
        return $function;
3397
    }
3398
3399
    /**
3400 19
     * Wrap string values in quotes.
3401
     */
3402
    public static function wrapResult(mixed $value): mixed
3403 11572
    {
3404
        if (is_string($value)) {
3405 11572
            //    Error values cannot be "wrapped"
3406 71
            if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
3407 71
                //    Return Excel errors "as is"
3408 69
                return $value;
3409 69
            }
3410 69
3411 66
            //    Return strings wrapped in quotes
3412
            return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
3413
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
3414
            //    Convert numeric errors to NaN error
3415
            return ExcelError::NAN();
3416 11572
        }
3417
3418
        return $value;
3419
    }
3420
3421
    /**
3422 11324
     * Remove quotes used as a wrapper to identify string values.
3423
     */
3424 11324
    public static function unwrapResult(mixed $value): mixed
3425
    {
3426 3905
        if (is_string($value)) {
3427
            if ((isset($value[0])) && ($value[0] == self::FORMULA_STRING_QUOTE) && (substr($value, -1) == self::FORMULA_STRING_QUOTE)) {
3428 1214
                return substr($value, 1, -1);
3429
            }
3430
            //    Convert numeric errors to NAN error
3431
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
3432 3140
            return ExcelError::NAN();
3433 8962
        }
3434
3435 4
        return $value;
3436
    }
3437
3438 8959
    /**
3439
     * Calculate cell value (using formula from a cell ID)
3440
     * Retained for backward compatibility.
3441
     *
3442
     * @param ?Cell $cell Cell to calculate
3443
     */
3444 11487
    public function calculate(?Cell $cell = null): mixed
3445
    {
3446 11487
        try {
3447 3595
            return $this->calculateCellValue($cell);
3448 2907
        } catch (\Exception $e) {
3449
            throw new Exception($e->getMessage());
3450
        }
3451 10249
    }
3452
3453
    /**
3454
     * Calculate the value of a cell formula.
3455 10313
     *
3456
     * @param ?Cell $cell Cell to calculate
3457
     * @param bool $resetLog Flag indicating whether the debug log should be reset or not
3458
     */
3459
    public function calculateCellValue(?Cell $cell = null, bool $resetLog = true): mixed
3460
    {
3461
        if ($cell === null) {
3462
            return null;
3463
        }
3464
3465
        if ($resetLog) {
3466
            //    Initialise the logging settings if requested
3467
            $this->formulaError = null;
3468
            $this->debugLog->clearLog();
3469
            $this->cyclicReferenceStack->clear();
3470
            $this->cyclicFormulaCounter = 1;
3471
        }
3472
3473
        //    Execute the calculation for the cell formula
3474
        $this->cellStack[] = [
3475
            'sheet' => $cell->getWorksheet()->getTitle(),
3476
            'cell' => $cell->getCoordinate(),
3477
        ];
3478
3479 7907
        $cellAddressAttempted = false;
3480
        $cellAddress = null;
3481 7907
3482
        try {
3483
            $value = $cell->getValue();
3484
            if ($cell->getDataType() === DataType::TYPE_FORMULA) {
3485 7907
                $value = preg_replace_callback(
3486
                    self::CALCULATION_REGEXP_CELLREF_SPILL,
3487 7896
                    fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
3488 7896
                    $value
3489 7896
                );
3490 7896
            }
3491
            $result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell));
3492
            if ($this->spreadsheet === null) {
3493
                throw new Exception('null spreadsheet in calculateCellValue');
3494 7907
            }
3495 7907
            $cellAddressAttempted = true;
3496 7907
            $cellAddress = array_pop($this->cellStack);
3497 7907
            if ($cellAddress === null) {
3498
                throw new Exception('null cellAddress in calculateCellValue');
3499 7907
            }
3500 7907
            $testSheet = $this->spreadsheet->getSheetByName($cellAddress['sheet']);
3501
            if ($testSheet === null) {
3502
                throw new Exception('worksheet not found in calculateCellValue');
3503 7907
            }
3504 7907
            $testSheet->getCell($cellAddress['cell']);
3505 7907
        } catch (\Exception $e) {
3506 7907
            if (!$cellAddressAttempted) {
3507 7907
                $cellAddress = array_pop($this->cellStack);
3508 7907
            }
3509 7907
            if ($this->spreadsheet !== null && is_array($cellAddress) && array_key_exists('sheet', $cellAddress)) {
3510
                $testSheet = $this->spreadsheet->getSheetByName($cellAddress['sheet']);
3511 7907
                if ($testSheet !== null && array_key_exists('cell', $cellAddress)) {
3512 7665
                    $testSheet->getCell($cellAddress['cell']);
3513
                }
3514
            }
3515 7665
3516 7665
            throw new Exception($e->getMessage(), $e->getCode(), $e);
3517 7665
        }
3518
3519
        if (is_array($result) && $this->getInstanceArrayReturnType() !== self::RETURN_ARRAY_AS_ARRAY) {
3520 7665
            $testResult = Functions::flattenArray($result);
3521 7665
            if ($this->getInstanceArrayReturnType() == self::RETURN_ARRAY_AS_ERROR) {
3522
                return ExcelError::VALUE();
3523
            }
3524 7665
            $result = array_shift($testResult);
3525 260
        }
3526 260
3527 260
        if ($result === null && $cell->getWorksheet()->getSheetView()->getShowZeros()) {
3528
            return 0;
3529 260
        } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
3530 260
            return ExcelError::NAN();
3531 260
        }
3532 260
3533
        return $result;
3534
    }
3535
3536 260
    /**
3537
     * Validate and parse a formula string.
3538
     *
3539 7665
     * @param string $formula Formula to parse
3540 4771
     */
3541 4771
    public function parseFormula(string $formula): array|bool
3542 1
    {
3543
        $formula = preg_replace_callback(
3544 4771
            self::CALCULATION_REGEXP_CELLREF_SPILL,
3545
            fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
3546
            $formula
3547 7665
        ) ?? $formula;
3548 16
        //    Basic validation that this is indeed a formula
3549 7664
        //    We return an empty array if not
3550
        $formula = trim($formula);
3551
        if ((!isset($formula[0])) || ($formula[0] != '=')) {
3552
            return [];
3553 7664
        }
3554
        $formula = ltrim(substr($formula, 1));
3555
        if (!isset($formula[0])) {
3556
            return [];
3557
        }
3558
3559
        //    Parse the formula and return the token stack
3560
        return $this->internalParseFormula($formula);
3561 7894
    }
3562
3563 7894
    /**
3564 7894
     * Calculate the value of a formula.
3565 7894
     *
3566 7894
     * @param string $formula Formula to parse
3567 7894
     * @param ?string $cellID Address of the cell to calculate
3568
     * @param ?Cell $cell Cell to calculate
3569
     */
3570 7894
    public function calculateFormula(string $formula, ?string $cellID = null, ?Cell $cell = null): mixed
3571 7894
    {
3572
        //    Initialise the logging settings
3573
        $this->formulaError = null;
3574 7894
        $this->debugLog->clearLog();
3575 7894
        $this->cyclicReferenceStack->clear();
3576
3577
        $resetCache = $this->getCalculationCacheEnabled();
3578
        if ($this->spreadsheet !== null && $cellID === null && $cell === null) {
3579
            $cellID = 'A1';
3580 7894
            $cell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
3581
        } else {
3582
            //    Disable calculation cacheing because it only applies to cell calculations, not straight formulae
3583
            //    But don't actually flush any cache
3584
            $this->calculationCacheEnabled = false;
3585
        }
3586
3587
        //    Execute the calculation
3588
        try {
3589
            $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $cell));
3590 174
        } catch (\Exception $e) {
3591
            throw new Exception($e->getMessage());
3592
        }
3593 174
3594 174
        if ($this->spreadsheet === null) {
3595 174
            //    Reset calculation cacheing to its previous state
3596
            $this->calculationCacheEnabled = $resetCache;
3597 174
        }
3598 174
3599 167
        return $result;
3600 167
    }
3601
3602
    public function getValueFromCache(string $cellReference, mixed &$cellValue): bool
3603
    {
3604 7
        $this->debugLog->writeDebugLog('Testing cache value for cell %s', $cellReference);
3605
        // Is calculation cacheing enabled?
3606
        // If so, is the required value present in calculation cache?
3607
        if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
3608
            $this->debugLog->writeDebugLog('Retrieving value for cell %s from cache', $cellReference);
3609 174
            // Return the cached result
3610 1
3611 1
            $cellValue = $this->calculationCache[$cellReference];
3612
3613
            return true;
3614 174
        }
3615
3616
        return false;
3617
    }
3618
3619 174
    public function saveValueToCache(string $cellReference, mixed $cellValue): void
3620
    {
3621
        if ($this->calculationCacheEnabled) {
3622 8052
            $this->calculationCache[$cellReference] = $cellValue;
3623
        }
3624 8052
    }
3625
3626
    /**
3627 8052
     * Parse a cell formula and calculate its value.
3628 307
     *
3629
     * @param string $formula The formula to parse and calculate
3630
     * @param ?string $cellID The ID (e.g. A3) of the cell that we are calculating
3631 307
     * @param ?Cell $cell Cell to calculate
3632
     * @param bool $ignoreQuotePrefix If set to true, evaluate the formyla even if the referenced cell is quote prefixed
3633 307
     */
3634
    public function _calculateFormulaValue(string $formula, ?string $cellID = null, ?Cell $cell = null, bool $ignoreQuotePrefix = false): mixed
3635
    {
3636 8052
        $cellValue = null;
3637
3638
        //  Quote-Prefixed cell values cannot be formulae, but are treated as strings
3639 7809
        if ($cell !== null && $ignoreQuotePrefix === false && $cell->getStyle()->getQuotePrefix() === true) {
3640
            return self::wrapResult((string) $formula);
3641 7809
        }
3642 7804
3643
        if (preg_match('/^=\s*cmd\s*\|/miu', $formula) !== 0) {
3644
            return self::wrapResult($formula);
3645
        }
3646
3647
        //    Basic validation that this is indeed a formula
3648
        //    We simply return the cell value if not
3649
        $formula = trim($formula);
3650
        if ($formula === '' || $formula[0] !== '=') {
3651
            return self::wrapResult($formula);
3652
        }
3653
        $formula = ltrim(substr($formula, 1));
3654 11751
        if (!isset($formula[0])) {
3655
            return self::wrapResult($formula);
3656 11751
        }
3657
3658
        $pCellParent = ($cell !== null) ? $cell->getWorksheet() : null;
3659 11751
        $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
3660 1
        $wsCellReference = $wsTitle . '!' . $cellID;
3661
3662
        if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
3663 11751
            return $cellValue;
3664
        }
3665
        $this->debugLog->writeDebugLog('Evaluating formula for cell %s', $wsCellReference);
3666
3667
        if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) {
3668
            if ($this->cyclicFormulaCount <= 0) {
3669 11751
                $this->cyclicFormulaCell = '';
3670 11751
3671 2
                return $this->raiseFormulaError('Cyclic Reference in Formula');
3672
            } elseif ($this->cyclicFormulaCell === $wsCellReference) {
3673 11751
                ++$this->cyclicFormulaCounter;
3674 11751
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
3675 5
                    $this->cyclicFormulaCell = '';
3676
3677
                    return $cellValue;
3678 11750
                }
3679 11750
            } elseif ($this->cyclicFormulaCell == '') {
3680 11750
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
3681
                    return $cellValue;
3682 11750
                }
3683 303
                $this->cyclicFormulaCell = $wsCellReference;
3684
            }
3685 11750
        }
3686
3687 11750
        $this->debugLog->writeDebugLog('Formula for cell %s is %s', $wsCellReference, $formula);
3688 12
        //    Parse the formula onto the token stack and calculate the value
3689 1
        $this->cyclicReferenceStack->push($wsCellReference);
3690
3691 1
        $cellValue = $this->processTokenStack($this->internalParseFormula($formula, $cell), $cellID, $cell);
3692 11
        $this->cyclicReferenceStack->pop();
3693 1
3694 1
        // Save to calculation cache
3695 1
        if ($cellID !== null) {
3696
            $this->saveValueToCache($wsCellReference, $cellValue);
3697 1
        }
3698
3699 11
        //    Return the calculated value
3700 11
        return $cellValue;
3701 10
    }
3702
3703 1
    /**
3704
     * Ensure that paired matrix operands are both matrices and of the same size.
3705
     *
3706
     * @param mixed $operand1 First matrix operand
3707 11750
     * @param mixed $operand2 Second matrix operand
3708
     * @param int $resize Flag indicating whether the matrices should be resized to match
3709 11750
     *                                        and (if so), whether the smaller dimension should grow or the
3710
     *                                        larger should shrink.
3711 11750
     *                                            0 = no resize
3712 11505
     *                                            1 = shrink to fit
3713
     *                                            2 = extend to fit
3714
     */
3715 11505
    public static function checkMatrixOperands(mixed &$operand1, mixed &$operand2, int $resize = 1): array
3716 7809
    {
3717
        //    Examine each of the two operands, and turn them into an array if they aren't one already
3718
        //    Note that this function should only be called if one or both of the operand is already an array
3719
        if (!is_array($operand1)) {
3720 11505
            [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand2);
3721
            $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1));
3722
            $resize = 0;
3723
        } elseif (!is_array($operand2)) {
3724
            [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand1);
3725
            $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2));
3726
            $resize = 0;
3727
        }
3728
3729
        [$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1);
3730
        [$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2);
3731
        if ($resize === 3) {
3732
            $resize = 2;
3733
        } elseif (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
3734
            $resize = 1;
3735 60
        }
3736
3737
        if ($resize == 2) {
3738
            //    Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger
3739 60
            self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
3740 18
        } elseif ($resize == 1) {
3741 18
            //    Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller
3742 18
            self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
3743 48
        }
3744 16
        [$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1);
3745 16
        [$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2);
3746 16
3747
        return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns];
3748
    }
3749 60
3750 60
    /**
3751 60
     * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0.
3752 22
     *
3753 41
     * @param array $matrix matrix operand
3754 33
     *
3755
     * @return int[] An array comprising the number of rows, and number of columns
3756
     */
3757 60
    public static function getMatrixDimensions(array &$matrix): array
3758
    {
3759 24
        $matrixRows = count($matrix);
3760 40
        $matrixColumns = 0;
3761
        foreach ($matrix as $rowKey => $rowValue) {
3762 33
            if (!is_array($rowValue)) {
3763
                $matrix[$rowKey] = [$rowValue];
3764 60
                $matrixColumns = max(1, $matrixColumns);
3765 60
            } else {
3766
                $matrix[$rowKey] = array_values($rowValue);
3767 60
                $matrixColumns = max(count($rowValue), $matrixColumns);
3768
            }
3769
        }
3770
        $matrix = array_values($matrix);
3771
3772
        return [$matrixRows, $matrixColumns];
3773
    }
3774
3775
    /**
3776
     * Ensure that paired matrix operands are both matrices of the same size.
3777 94
     *
3778
     * @param array $matrix1 First matrix operand
3779 94
     * @param array $matrix2 Second matrix operand
3780 94
     * @param int $matrix1Rows Row size of first matrix operand
3781 94
     * @param int $matrix1Columns Column size of first matrix operand
3782 92
     * @param int $matrix2Rows Row size of second matrix operand
3783 4
     * @param int $matrix2Columns Column size of second matrix operand
3784 4
     */
3785
    private static function resizeMatricesShrink(array &$matrix1, array &$matrix2, int $matrix1Rows, int $matrix1Columns, int $matrix2Rows, int $matrix2Columns): void
3786 88
    {
3787 88
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3788
            if ($matrix2Rows < $matrix1Rows) {
3789
                for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) {
3790 94
                    unset($matrix1[$i]);
3791
                }
3792 94
            }
3793
            if ($matrix2Columns < $matrix1Columns) {
3794
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3795
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3796
                        unset($matrix1[$i][$j]);
3797
                    }
3798
                }
3799
            }
3800
        }
3801
3802
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3803
            if ($matrix1Rows < $matrix2Rows) {
3804
                for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) {
3805 33
                    unset($matrix2[$i]);
3806
                }
3807 33
            }
3808
            if ($matrix1Columns < $matrix2Columns) {
3809
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3810
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3811
                        unset($matrix2[$i][$j]);
3812
                    }
3813
                }
3814
            }
3815
        }
3816
    }
3817
3818
    /**
3819
     * Ensure that paired matrix operands are both matrices of the same size.
3820
     *
3821
     * @param array $matrix1 First matrix operand
3822 33
     * @param array $matrix2 Second matrix operand
3823
     * @param int $matrix1Rows Row size of first matrix operand
3824
     * @param int $matrix1Columns Column size of first matrix operand
3825
     * @param int $matrix2Rows Row size of second matrix operand
3826
     * @param int $matrix2Columns Column size of second matrix operand
3827
     */
3828
    private static function resizeMatricesExtend(array &$matrix1, array &$matrix2, int $matrix1Rows, int $matrix1Columns, int $matrix2Rows, int $matrix2Columns): void
3829
    {
3830
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3831
            if ($matrix2Columns < $matrix1Columns) {
3832
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3833
                    $x = $matrix2[$i][$matrix2Columns - 1];
3834
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3835
                        $matrix2[$i][$j] = $x;
3836
                    }
3837
                }
3838
            }
3839
            if ($matrix2Rows < $matrix1Rows) {
3840
                $x = $matrix2[$matrix2Rows - 1];
3841
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3842
                    $matrix2[$i] = $x;
3843
                }
3844
            }
3845
        }
3846
3847
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3848 24
            if ($matrix1Columns < $matrix2Columns) {
3849
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3850 24
                    $x = $matrix1[$i][$matrix1Columns - 1];
3851 16
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3852 15
                        $matrix1[$i][$j] = $x;
3853 15
                    }
3854 15
                }
3855 15
            }
3856
            if ($matrix1Rows < $matrix2Rows) {
3857
                $x = $matrix1[$matrix1Rows - 1];
3858
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3859 16
                    $matrix1[$i] = $x;
3860 2
                }
3861 2
            }
3862 2
        }
3863
    }
3864
3865
    /**
3866
     * Format details of an operand for display in the log (based on operand type).
3867 24
     *
3868 15
     * @param mixed $value First matrix operand
3869
     */
3870
    private function showValue(mixed $value): mixed
3871
    {
3872
        if ($this->debugLog->getWriteDebugLog()) {
3873
            $testArray = Functions::flattenArray($value);
3874
            if (count($testArray) == 1) {
3875
                $value = array_pop($testArray);
3876 15
            }
3877 15
3878 15
            if (is_array($value)) {
3879 15
                $returnMatrix = [];
3880
                $pad = $rpad = ', ';
3881
                foreach ($value as $row) {
3882
                    if (is_array($row)) {
3883
                        $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
3884
                        $rpad = '; ';
3885
                    } else {
3886
                        $returnMatrix[] = $this->showValue($row);
3887
                    }
3888
                }
3889
3890 11493
                return '{ ' . implode($rpad, $returnMatrix) . ' }';
3891
            } elseif (is_string($value) && (trim($value, self::FORMULA_STRING_QUOTE) == $value)) {
3892 11493
                return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
3893 3
            } elseif (is_bool($value)) {
3894 3
                return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3895 3
            } elseif ($value === null) {
3896
                return self::$localeBoolean['NULL'];
3897
            }
3898 3
        }
3899 2
3900 2
        return Functions::flattenSingleValue($value);
3901 2
    }
3902 2
3903 2
    /**
3904 2
     * Format type and details of an operand for display in the log (based on operand type).
3905
     *
3906
     * @param mixed $value First matrix operand
3907
     */
3908
    private function showTypeDetails(mixed $value): ?string
3909
    {
3910 2
        if ($this->debugLog->getWriteDebugLog()) {
3911 3
            $testArray = Functions::flattenArray($value);
3912 2
            if (count($testArray) == 1) {
3913 3
                $value = array_pop($testArray);
3914
            }
3915 3
3916
            if ($value === null) {
3917
                return 'a NULL value';
3918
            } elseif (is_float($value)) {
3919
                $typeString = 'a floating point number';
3920 11493
            } elseif (is_int($value)) {
3921
                $typeString = 'an integer number';
3922
            } elseif (is_bool($value)) {
3923
                $typeString = 'a boolean';
3924
            } elseif (is_array($value)) {
3925
                $typeString = 'a matrix';
3926
            } else {
3927
                if ($value == '') {
3928 11507
                    return 'an empty string';
3929
                } elseif ($value[0] == '#') {
3930 11507
                    return 'a ' . $value . ' error';
3931 3
                }
3932 3
                $typeString = 'a string';
3933 3
            }
3934
3935
            return $typeString . ' with a value of ' . $this->showValue($value);
3936 3
        }
3937
3938 3
        return null;
3939 3
    }
3940 3
3941 3
    /**
3942 2
     * @return false|string False indicates an error
3943
     */
3944 2
    private function convertMatrixReferences(string $formula): false|string
3945 2
    {
3946
        static $matrixReplaceFrom = [self::FORMULA_OPEN_MATRIX_BRACE, ';', self::FORMULA_CLOSE_MATRIX_BRACE];
3947
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
3948
3949
        //    Convert any Excel matrix references to the MKMATRIX() function
3950
        if (str_contains($formula, self::FORMULA_OPEN_MATRIX_BRACE)) {
3951
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
3952
            if (str_contains($formula, self::FORMULA_STRING_QUOTE)) {
3953
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
3954
                //        the formula
3955 3
                $temp = explode(self::FORMULA_STRING_QUOTE, $formula);
3956
                //    Open and Closed counts used for trapping mismatched braces in the formula
3957
                $openCount = $closeCount = 0;
3958 11504
                $notWithinQuotes = false;
3959
                foreach ($temp as &$value) {
3960
                    //    Only count/replace in alternating array entries
3961
                    $notWithinQuotes = $notWithinQuotes === false;
3962
                    if ($notWithinQuotes === true) {
3963
                        $openCount += substr_count($value, self::FORMULA_OPEN_MATRIX_BRACE);
3964 11884
                        $closeCount += substr_count($value, self::FORMULA_CLOSE_MATRIX_BRACE);
3965
                        $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
3966 11884
                    }
3967 11884
                }
3968
                unset($value);
3969
                //    Then rebuild the formula string
3970 11884
                $formula = implode(self::FORMULA_STRING_QUOTE, $temp);
3971
            } else {
3972 781
                //    If there's no quoted strings, then we do a simple count/replace
3973
                $openCount = substr_count($formula, self::FORMULA_OPEN_MATRIX_BRACE);
3974
                $closeCount = substr_count($formula, self::FORMULA_CLOSE_MATRIX_BRACE);
3975 241
                $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
3976
            }
3977 241
            //    Trap for mismatched braces and trigger an appropriate error
3978 241
            if ($openCount < $closeCount) {
3979 241
                if ($openCount > 0) {
3980
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
3981 241
                }
3982 241
3983 241
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
3984 241
            } elseif ($openCount > $closeCount) {
3985 241
                if ($closeCount > 0) {
3986
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
3987
                }
3988 241
3989
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
3990 241
            }
3991
        }
3992
3993 541
        return $formula;
3994 541
    }
3995 541
3996
    /**
3997
     *    Binary Operators.
3998 781
     *    These operators always work on two values.
3999
     *    Array key is the operator, the value indicates whether this is a left or right associative operator.
4000
     */
4001
    private static array $operatorAssociativity = [
4002
        '^' => 0, //    Exponentiation
4003
        '*' => 0, '/' => 0, //    Multiplication and Division
4004 781
        '+' => 0, '-' => 0, //    Addition and Subtraction
4005
        '&' => 0, //    Concatenation
4006
        '∪' => 0, '∩' => 0, ':' => 0, //    Union, Intersect and Range
4007
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
4008
    ];
4009
4010
    /**
4011
     *    Comparison (Boolean) Operators.
4012
     *    These operators work on two values, but always return a boolean result.
4013 11884
     */
4014
    private static array $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
4015
4016
    /**
4017
     *    Operator Precedence.
4018
     *    This list includes all valid operators, whether binary (including boolean) or unary (such as %).
4019
     *    Array key is the operator, the value is its precedence.
4020
     */
4021
    private static array $operatorPrecedence = [
4022
        ':' => 9, //    Range
4023
        '∩' => 8, //    Intersect
4024
        '∪' => 7, //    Union
4025
        '~' => 6, //    Negation
4026
        '%' => 5, //    Percentage
4027
        '^' => 4, //    Exponentiation
4028
        '*' => 3, '/' => 3, //    Multiplication and Division
4029
        '+' => 2, '-' => 2, //    Addition and Subtraction
4030
        '&' => 1, //    Concatenation
4031
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
4032
    ];
4033
4034
    // Convert infix to postfix notation
4035
4036
    /**
4037
     * @return array<int, mixed>|false
4038
     */
4039
    private function internalParseFormula(string $formula, ?Cell $cell = null): bool|array
4040
    {
4041
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
4042
            return false;
4043
        }
4044
4045
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
4046
        //        so we store the parent worksheet so that we can re-attach it when necessary
4047
        $pCellParent = ($cell !== null) ? $cell->getWorksheet() : null;
4048
4049
        $regexpMatchString = '/^((?<string>' . self::CALCULATION_REGEXP_STRING
4050
                                . ')|(?<function>' . self::CALCULATION_REGEXP_FUNCTION
4051
                                . ')|(?<cellRef>' . self::CALCULATION_REGEXP_CELLREF
4052
                                . ')|(?<colRange>' . self::CALCULATION_REGEXP_COLUMN_RANGE
4053
                                . ')|(?<rowRange>' . self::CALCULATION_REGEXP_ROW_RANGE
4054
                                . ')|(?<number>' . self::CALCULATION_REGEXP_NUMBER
4055
                                . ')|(?<openBrace>' . self::CALCULATION_REGEXP_OPENBRACE
4056
                                . ')|(?<structuredReference>' . self::CALCULATION_REGEXP_STRUCTURED_REFERENCE
4057
                                . ')|(?<definedName>' . self::CALCULATION_REGEXP_DEFINEDNAME
4058
                                . ')|(?<error>' . self::CALCULATION_REGEXP_ERROR
4059 11884
                                . '))/sui';
4060
4061 11884
        //    Start with initialisation
4062
        $index = 0;
4063
        $stack = new Stack($this->branchPruner);
4064
        $output = [];
4065
        $expectingOperator = false; //    We use this test in syntax-checking the expression to determine when a
4066
        //        - is a negation or + is a positive operator rather than an operation
4067 11884
        $expectingOperand = false; //    We use this test in syntax-checking the expression to determine whether an operand
4068
        //        should be null in a function call
4069 11884
4070 11884
        //    The guts of the lexical parser
4071 11884
        //    Loop through the formula extracting each operator and operand in turn
4072 11884
        while (true) {
4073 11884
            // Branch pruning: we adapt the output item to the context (it will
4074 11884
            // be used to limit its computation)
4075 11884
            $this->branchPruner->initialiseForLoop();
4076 11884
4077 11884
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
4078 11884
4079 11884
            // Check for two-character operators (e.g. >=, <=, <>)
4080
            if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && isset($formula[$index + 1], self::$comparisonOperators[$formula[$index + 1]])) {
4081
                $opCharacter .= $formula[++$index];
4082 11884
            }
4083 11884
            //    Find out if we're currently at the beginning of a number, variable, cell/row/column reference,
4084 11884
            //         function, defined name, structured reference, parenthesis, error or operand
4085 11884
            $isOperandOrFunction = (bool) preg_match($regexpMatchString, substr($formula, $index), $match);
4086
4087 11884
            $expectingOperatorCopy = $expectingOperator;
4088
            if ($opCharacter === '-' && !$expectingOperator) {                //    Is it a negation instead of a minus?
4089
                //    Put a negation on the stack
4090
                $stack->push('Unary Operator', '~');
4091
                ++$index; //        and drop the negation symbol
4092 11884
            } elseif ($opCharacter === '%' && $expectingOperator) {
4093
                //    Put a percentage on the stack
4094
                $stack->push('Unary Operator', '%');
4095 11884
                ++$index;
4096
            } elseif ($opCharacter === '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
4097 11884
                ++$index; //    Drop the redundant plus symbol
4098
            } elseif ((($opCharacter === '~') || ($opCharacter === '∩') || ($opCharacter === '∪')) && (!$isOperandOrFunction)) {
4099
                //    We have to explicitly deny a tilde, union or intersect because they are legal
4100 11884
                return $this->raiseFormulaError("Formula Error: Illegal character '~'"); //        on the stack but not in the input expression
4101 80
            } elseif ((isset(self::CALCULATION_OPERATORS[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) {    //    Are we putting an operator on the stack?
4102
                while (
4103
                    $stack->count() > 0
4104
                    && ($o2 = $stack->last())
4105 11884
                    && isset(self::CALCULATION_OPERATORS[$o2['value']])
4106
                    && @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])
4107 11884
                ) {
4108 11884
                    $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
4109
                }
4110 1115
4111 1115
                //    Finally put our current operator onto the stack
4112 11884
                $stack->push('Binary Operator', $opCharacter);
4113
4114 8
                ++$index;
4115 8
                $expectingOperator = false;
4116 11884
            } elseif ($opCharacter === ')' && $expectingOperator) { //    Are we expecting to close a parenthesis?
4117 6
                $expectingOperand = false;
4118 11884
                while (($o2 = $stack->pop()) && $o2['value'] !== '(') { //    Pop off the stack back to the last (
4119
                    $output[] = $o2;
4120
                }
4121 11884
                $d = $stack->last(2);
4122
4123 1662
                // Branch pruning we decrease the depth whether is it a function
4124 1662
                // call or a parenthesis
4125 1662
                $this->branchPruner->decrementDepth();
4126 1662
4127
                if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) {
4128 82
                    //    Did this parenthesis just close a function?
4129
                    try {
4130
                        $this->branchPruner->closingBrace($d['value']);
4131
                    } catch (Exception $e) {
4132 1662
                        return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
4133
                    }
4134 1662
4135 1662
                    $functionName = $matches[1]; //    Get the function name
4136 11884
                    $d = $stack->pop();
4137 11540
                    $argumentCount = $d['value'] ?? 0; //    See how many arguments there were (argument count is the next value stored on the stack)
4138 11540
                    $output[] = $d; //    Dump the argument count on the output
4139 1327
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
4140
                    if (isset(self::$controlFunctions[$functionName])) {
4141 11540
                        $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
4142
                    } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
4143
                        $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
4144
                    } else {    // did we somehow push a non-function on the stack? this should never happen
4145 11540
                        return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack');
4146
                    }
4147 11540
                    //    Check the argument count
4148
                    $argumentCountError = false;
4149
                    $expectedArgumentCountString = null;
4150 11536
                    if (is_numeric($expectedArgumentCount)) {
4151 4
                        if ($expectedArgumentCount < 0) {
4152 4
                            if ($argumentCount > abs($expectedArgumentCount)) {
4153
                                $argumentCountError = true;
4154
                                $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
4155 11536
                            }
4156 11536
                        } else {
4157 11536
                            if ($argumentCount != $expectedArgumentCount) {
4158 11536
                                $argumentCountError = true;
4159 11536
                                $expectedArgumentCountString = $expectedArgumentCount;
4160 11536
                            }
4161 800
                        }
4162 11533
                    } elseif ($expectedArgumentCount != '*') {
4163 11533
                        preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch);
4164
                        switch ($argMatch[2] ?? '') {
4165
                            case '+':
4166
                                if ($argumentCount < $argMatch[1]) {
4167
                                    $argumentCountError = true;
4168 11536
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
4169 11536
                                }
4170 11536
4171 5847
                                break;
4172 36
                            case '-':
4173
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
4174
                                    $argumentCountError = true;
4175
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
4176
                                }
4177 5813
4178 142
                                break;
4179 142
                            case ',':
4180
                                if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
4181
                                    $argumentCountError = true;
4182 6436
                                    $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
4183 5961
                                }
4184 5961
4185 5961
                                break;
4186 1099
                        }
4187 25
                    }
4188 25
                    if ($argumentCountError) {
4189
                        return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected');
4190
                    }
4191 1099
                }
4192 5021
                ++$index;
4193 811
            } elseif ($opCharacter === ',') { // Is this the separator for function arguments?
4194 12
                try {
4195 12
                    $this->branchPruner->argumentSeparator();
4196
                } catch (Exception $e) {
4197
                    return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
4198 811
                }
4199 4248
4200 4248
                while (($o2 = $stack->pop()) && $o2['value'] !== '(') {        //    Pop off the stack back to the last (
4201 39
                    $output[] = $o2; // pop the argument expression stuff and push onto the output
4202 39
                }
4203
                //    If we've a comma when we're expecting an operand, then what we actually have is a null operand;
4204
                //        so push a null onto the stack
4205 4248
                if (($expectingOperand) || (!$expectingOperator)) {
4206
                    $output[] = $stack->getStackItem('Empty Argument', null, 'NULL');
4207
                }
4208 11536
                // make sure there was a function
4209 218
                $d = $stack->last(2);
4210
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'] ?? '', $matches)) {
4211
                    // Can we inject a dummy function at this point so that the braces at least have some context
4212 11325
                    //     because at least the braces are paired up (at this stage in the formula)
4213 11884
                    // MS Excel allows this if the content is cell references; but doesn't allow actual values,
4214
                    //    but at this point, we can't differentiate (so allow both)
4215 7853
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
4216
                }
4217
4218
                /** @var array $d */
4219
                $d = $stack->pop();
4220 7853
                ++$d['value']; // increment the argument count
4221 1362
4222
                $stack->pushStackItem($d);
4223
                $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again
4224
4225 7853
                $expectingOperator = false;
4226 106
                $expectingOperand = true;
4227
                ++$index;
4228
            } elseif ($opCharacter === '(' && !$expectingOperator) {
4229 7853
                // Branch pruning: we go deeper
4230 7853
                $this->branchPruner->incrementDepth();
4231
                $stack->push('Brace', '(', null);
4232
                ++$index;
4233
            } elseif ($isOperandOrFunction && !$expectingOperatorCopy) {
4234
                // do we now have a function/variable/number?
4235
                $expectingOperator = true;
4236
                $expectingOperand = false;
4237
                $val = $match[1];
4238
                $length = strlen($val);
4239 7853
4240 7853
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) {
4241
                    $val = (string) preg_replace('/\s/u', '', $val);
4242 7853
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
4243 7853
                        $valToUpper = strtoupper($val);
4244
                    } else {
4245 7853
                        $valToUpper = 'NAME.ERROR(';
4246 7853
                    }
4247 7853
                    // here $matches[1] will contain values like "IF"
4248 11884
                    // and $val "IF("
4249
4250 29
                    $this->branchPruner->functionCall($valToUpper);
4251 29
4252 29
                    $stack->push('Function', $valToUpper);
4253 11884
                    // tests if the function is closed right after opening
4254
                    $ax = preg_match('/^\s*\)/u', substr($formula, $index + $length));
4255 11880
                    if ($ax) {
4256 11880
                        $stack->push('Operand Count for Function ' . $valToUpper . ')', 0);
4257 11880
                        $expectingOperator = true;
4258 11880
                    } else {
4259
                        $stack->push('Operand Count for Function ' . $valToUpper . ')', 1);
4260 11880
                        $expectingOperator = false;
4261 11542
                    }
4262 11542
                    $stack->push('Brace', '(');
4263 11540
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $val, $matches)) {
4264
                    //    Watch for this case-change when modifying to allow cell references in different worksheets...
4265 4
                    //    Should only be applied to the actual cell column, not the worksheet name
4266
                    //    If the last entry on the stack was a : operator, then we have a cell range reference
4267
                    $testPrevOp = $stack->last(1);
4268
                    if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
4269
                        //    If we have a worksheet reference, then we're playing with a 3D reference
4270 11542
                        if ($matches[2] === '') {
4271
                            //    Otherwise, we 'inherit' the worksheet reference from the start cell reference
4272 11542
                            //    The start of the cell range reference should be the last entry in $output
4273
                            $rangeStartCellRef = $output[count($output) - 1]['value'] ?? '';
4274 11542
                            if ($rangeStartCellRef === ':') {
4275 11542
                                // Do we have chained range operators?
4276 321
                                $rangeStartCellRef = $output[count($output) - 2]['value'] ?? '';
4277 321
                            }
4278
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches);
4279 11364
                            if (array_key_exists(2, $rangeStartMatches)) {
4280 11364
                                if ($rangeStartMatches[2] > '') {
4281
                                    $val = $rangeStartMatches[2] . '!' . $val;
4282 11542
                                }
4283 11679
                            } else {
4284
                                $val = ExcelError::REF();
4285
                            }
4286
                        } else {
4287 6852
                            $rangeStartCellRef = $output[count($output) - 1]['value'] ?? '';
4288 6852
                            if ($rangeStartCellRef === ':') {
4289
                                // Do we have chained range operators?
4290 1110
                                $rangeStartCellRef = $output[count($output) - 2]['value'] ?? '';
4291
                            }
4292
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches);
4293 1106
                            if ($rangeStartMatches[2] !== $matches[2]) {
4294 1106
                                return $this->raiseFormulaError('3D Range references are not yet supported');
4295
                            }
4296 5
                        }
4297
                    } elseif (!str_contains($val, '!') && $pCellParent !== null) {
4298 1106
                        $worksheet = $pCellParent->getTitle();
4299 1106
                        $val = "'{$worksheet}'!{$val}";
4300 1101
                    }
4301 1084
                    // unescape any apostrophes or double quotes in worksheet name
4302
                    $val = str_replace(["''", '""'], ["'", '"'], $val);
4303
                    $outputItem = $stack->getStackItem('Cell Reference', $val, $val);
4304 5
4305
                    $output[] = $outputItem;
4306
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_STRUCTURED_REFERENCE . '$/miu', $val, $matches)) {
4307 4
                    try {
4308 4
                        $structuredReference = Operands\StructuredReference::fromParser($formula, $index, $matches);
4309
                    } catch (Exception $e) {
4310
                        return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
4311
                    }
4312 4
4313 4
                    $val = $structuredReference->value();
4314 2
                    $length = strlen($val);
4315
                    $outputItem = $stack->getStackItem(Operands\StructuredReference::NAME, $structuredReference, null);
4316
4317 6847
                    $output[] = $outputItem;
4318 6653
                    $expectingOperator = true;
4319 6653
                } else {
4320
                    // it's a variable, constant, string, number or boolean
4321
                    $localeConstant = false;
4322 6852
                    $stackItemType = 'Value';
4323 6852
                    $stackItemReference = null;
4324
4325 6852
                    //    If the last entry on the stack was a : operator, then we may have a row or column range reference
4326 6164
                    $testPrevOp = $stack->last(1);
4327
                    if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
4328 75
                        $stackItemType = 'Cell Reference';
4329
4330
                        if (
4331
                            !is_numeric($val)
4332
                            && ((ctype_alpha($val) === false || strlen($val) > 3))
4333 75
                            && (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $val) !== false)
4334 75
                            && ($this->spreadsheet === null || $this->spreadsheet->getNamedRange($val) !== null)
4335 75
                        ) {
4336
                            $namedRange = ($this->spreadsheet === null) ? null : $this->spreadsheet->getNamedRange($val);
4337 75
                            if ($namedRange !== null) {
4338 75
                                $stackItemType = 'Defined Name';
4339
                                $address = str_replace('$', '', $namedRange->getValue());
4340
                                $stackItemReference = $val;
4341 6098
                                if (str_contains($address, ':')) {
4342 6098
                                    // We'll need to manipulate the stack for an actual named range rather than a named cell
4343 6098
                                    $fromTo = explode(':', $address);
4344
                                    $to = array_pop($fromTo);
4345
                                    foreach ($fromTo as $from) {
4346 6098
                                        $output[] = $stack->getStackItem($stackItemType, $from, $stackItemReference);
4347 6098
                                        $output[] = $stack->getStackItem('Binary Operator', ':');
4348 34
                                    }
4349
                                    $address = $to;
4350
                                }
4351 34
                                $val = $address;
4352 34
                            }
4353 34
                        } elseif ($val === ExcelError::REF()) {
4354 34
                            $stackItemReference = $val;
4355
                        } else {
4356 10
                            /** @var non-empty-string $startRowColRef */
4357 10
                            $startRowColRef = $output[count($output) - 1]['value'] ?? '';
4358 4
                            [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
4359 4
                            $rangeSheetRef = $rangeWS1;
4360 4
                            if ($rangeWS1 !== '') {
4361 4
                                $rangeWS1 .= '!';
4362
                            }
4363 3
                            $rangeSheetRef = trim($rangeSheetRef, "'");
4364 3
                            [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
4365 3
                            if ($rangeWS2 !== '') {
4366 3
                                $rangeWS2 .= '!';
4367 3
                            } else {
4368
                                $rangeWS2 = $rangeWS1;
4369 3
                            }
4370
4371 4
                            $refSheet = $pCellParent;
4372
                            if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) {
4373 30
                                $refSheet = $pCellParent->getParentOrThrow()->getSheetByName($rangeSheetRef);
4374 3
                            }
4375
4376
                            if (ctype_digit($val) && $val <= 1048576) {
4377 27
                                //    Row range
4378 27
                                $stackItemType = 'Row Reference';
4379 27
                                /** @var int $valx */
4380 27
                                $valx = $val;
4381 18
                                $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : AddressRange::MAX_COLUMN; //    Max 16,384 columns for Excel2007
4382
                                $val = "{$rangeWS2}{$endRowColRef}{$val}";
4383 27
                            } elseif (ctype_alpha($val) && strlen($val ?? '') <= 3) {
4384 27
                                //    Column range
4385 27
                                $stackItemType = 'Column Reference';
4386
                                $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : AddressRange::MAX_ROW; //    Max 1,048,576 rows for Excel2007
4387
                                $val = "{$rangeWS2}{$val}{$endRowColRef}";
4388 27
                            }
4389
                            $stackItemReference = $val;
4390
                        }
4391 27
                    } elseif ($opCharacter === self::FORMULA_STRING_QUOTE) {
4392 27
                        //    UnEscape any quotes within the string
4393 4
                        $val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
4394
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
4395
                        $stackItemType = 'Constant';
4396 27
                        $excelConstant = trim(strtoupper($val));
4397
                        $val = self::$excelConstants[$excelConstant];
4398 8
                        $stackItemReference = $excelConstant;
4399
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
4400 8
                        $stackItemType = 'Constant';
4401 8
                        $val = self::$excelConstants[$localeConstant];
4402 8
                        $stackItemReference = $localeConstant;
4403 19
                    } elseif (
4404
                        preg_match('/^' . self::CALCULATION_REGEXP_ROW_RANGE . '/miu', substr($formula, $index), $rowRangeReference)
4405 14
                    ) {
4406 14
                        $val = $rowRangeReference[1];
4407 14
                        $length = strlen($rowRangeReference[1]);
4408
                        $stackItemType = 'Row Reference';
4409 27
                        // unescape any apostrophes or double quotes in worksheet name
4410
                        $val = str_replace(["''", '""'], ["'", '"'], $val);
4411 6093
                        $column = 'A';
4412
                        if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
4413 2760
                            $column = $pCellParent->getHighestDataColumn($val);
4414 4506
                        }
4415 535
                        $val = "{$rowRangeReference[2]}{$column}{$rowRangeReference[7]}";
4416 535
                        $stackItemReference = $val;
4417 535
                    } elseif (
4418 535
                        preg_match('/^' . self::CALCULATION_REGEXP_COLUMN_RANGE . '/miu', substr($formula, $index), $columnRangeReference)
4419 4229
                    ) {
4420 37
                        $val = $columnRangeReference[1];
4421 37
                        $length = strlen($val);
4422 37
                        $stackItemType = 'Column Reference';
4423
                        // unescape any apostrophes or double quotes in worksheet name
4424 4210
                        $val = str_replace(["''", '""'], ["'", '"'], $val);
4425
                        $row = '1';
4426 8
                        if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
4427 8
                            $row = $pCellParent->getHighestDataRow($val);
4428 8
                        }
4429
                        $val = "{$val}{$row}";
4430 8
                        $stackItemReference = $val;
4431 8
                    } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', $val, $match)) {
4432 8
                        $stackItemType = 'Defined Name';
4433
                        $stackItemReference = $val;
4434
                    } elseif (is_numeric($val)) {
4435 8
                        if ((str_contains((string) $val, '.')) || (stripos((string) $val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
4436 8
                            $val = (float) $val;
4437
                        } else {
4438 4203
                            $val = (int) $val;
4439
                        }
4440 14
                    }
4441 14
4442 14
                    $details = $stack->getStackItem($stackItemType, $val, $stackItemReference);
4443
                    if ($localeConstant) {
4444 14
                        $details['localeValue'] = $localeConstant;
4445 14
                    }
4446 14
                    $output[] = $details;
4447
                }
4448
                $index += $length;
4449 14
            } elseif ($opCharacter === '$') { // absolute row or column range
4450 14
                ++$index;
4451 4189
            } elseif ($opCharacter === ')') { // miscellaneous error checking
4452 136
                if ($expectingOperand) {
4453 136
                    $output[] = $stack->getStackItem('Empty Argument', null, 'NULL');
4454 4077
                    $expectingOperand = false;
4455 4071
                    $expectingOperator = true;
4456 1656
                } else {
4457
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
4458 3321
                }
4459
            } elseif (isset(self::CALCULATION_OPERATORS[$opCharacter]) && !$expectingOperator) {
4460
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
4461
            } else {    // I don't even want to know what you did to get here
4462 6098
                return $this->raiseFormulaError('Formula Error: An unexpected error occurred');
4463 6098
            }
4464 37
            //    Test for end of formula string
4465
            if ($index == strlen($formula)) {
4466 6098
                //    Did we end with an operator?.
4467
                //    Only valid for the % unary operator
4468 11880
                if ((isset(self::CALCULATION_OPERATORS[$opCharacter])) && ($opCharacter != '%')) {
4469 95
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
4470 6
                }
4471 89
4472 83
                break;
4473 83
            }
4474 83
            //    Ignore white space
4475 83
            while (($formula[$index] === "\n") || ($formula[$index] === "\r")) {
4476
                ++$index;
4477
            }
4478
4479 6
            if ($formula[$index] === ' ') {
4480
                while ($formula[$index] === ' ') {
4481
                    ++$index;
4482 6
                }
4483
4484
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
4485 11880
                //        Cell References, Defined Names or Structured References) then we have an INTERSECTION operator
4486
                $countOutputMinus1 = count($output) - 1;
4487
                if (
4488 11661
                    ($expectingOperator)
4489 1
                    && array_key_exists($countOutputMinus1, $output)
4490
                    && is_array($output[$countOutputMinus1])
4491
                    && array_key_exists('type', $output[$countOutputMinus1])
4492 11660
                    && (
4493
                        (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/miu', substr($formula, $index), $match))
4494
                            && ($output[$countOutputMinus1]['type'] === 'Cell Reference')
4495 11856
                        || (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', substr($formula, $index), $match))
4496
                            && ($output[$countOutputMinus1]['type'] === 'Defined Name' || $output[$countOutputMinus1]['type'] === 'Value')
4497
                        || (preg_match('/^' . self::CALCULATION_REGEXP_STRUCTURED_REFERENCE . '.*/miu', substr($formula, $index), $match))
4498
                            && ($output[$countOutputMinus1]['type'] === Operands\StructuredReference::NAME || $output[$countOutputMinus1]['type'] === 'Value')
4499 11856
                    )
4500 1941
                ) {
4501 1941
                    while (
4502
                        $stack->count() > 0
4503
                        && ($o2 = $stack->last())
4504
                        && isset(self::CALCULATION_OPERATORS[$o2['value']])
4505
                        && @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])
4506 1941
                    ) {
4507
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
4508 1941
                    }
4509 1941
                    $stack->push('Binary Operator', '∩'); //    Put an Intersect Operator on the stack
4510 1941
                    $expectingOperator = false;
4511 1941
                }
4512
            }
4513 1941
        }
4514 1941
4515 1941
        while (($op = $stack->pop()) !== null) {
4516 1941
            // pop everything off the stack and push onto output
4517 1941
            if ((is_array($op) && $op['value'] == '(')) {
4518 1941
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
4519
            }
4520
            $output[] = $op;
4521
        }
4522 18
4523 18
        return $output;
4524 18
    }
4525 18
4526
    private static function dataTestReference(array &$operandData): mixed
4527 11
    {
4528
        $operand = $operandData['value'];
4529 18
        if (($operandData['reference'] === null) && (is_array($operand))) {
4530 18
            $rKeys = array_keys($operand);
4531
            $rowKey = array_shift($rKeys);
4532
            if (is_array($operand[$rowKey]) === false) {
4533
                $operandData['value'] = $operand[$rowKey];
4534
4535 11660
                return $operand[$rowKey];
4536
            }
4537 674
4538 4
            $cKeys = array_keys(array_keys($operand[$rowKey]));
4539
            $colKey = array_shift($cKeys);
4540 672
            if (ctype_upper("$colKey")) {
4541
                $operandData['reference'] = $colKey . $rowKey;
4542
            }
4543 11658
        }
4544
4545
        return $operand;
4546 1550
    }
4547
4548 1550
    /**
4549 1550
     * @return array<int, mixed>|false
4550 41
     */
4551 41
    private function processTokenStack(mixed $tokens, ?string $cellID = null, ?Cell $cell = null)
4552 41
    {
4553 5
        if ($tokens === false) {
4554
            return false;
4555 5
        }
4556
4557
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
4558 40
        //        so we store the parent cell collection so that we can re-attach it when necessary
4559 40
        $pCellWorksheet = ($cell !== null) ? $cell->getWorksheet() : null;
4560 40
        $originalCoordinate = $cell?->getCoordinate();
4561
        $pCellParent = ($cell !== null) ? $cell->getParent() : null;
4562
        $stack = new Stack($this->branchPruner);
4563
4564
        // Stores branches that have been pruned
4565 1550
        $fakedForBranchPruning = [];
4566
        // help us to know when pruning ['branchTestId' => true/false]
4567
        $branchStore = [];
4568
        //    Loop through each token in turn
4569
        foreach ($tokens as $tokenIdx => $tokenData) {
4570
            $this->processingAnchorArray = false;
4571 11532
            if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') {
4572
                $this->processingAnchorArray = true;
4573 11532
            }
4574 2
            $token = $tokenData['value'];
4575
            // Branch pruning: skip useless resolutions
4576
            $storeKey = $tokenData['storeKey'] ?? null;
4577
            if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) {
4578
                $onlyIfStoreKey = $tokenData['onlyIf'];
4579 11531
                $storeValue = $branchStore[$onlyIfStoreKey] ?? null;
4580 11531
                $storeValueAsBool = ($storeValue === null)
4581 11531
                    ? true : (bool) Functions::flattenSingleValue($storeValue);
4582 11531
                if (is_array($storeValue)) {
4583
                    $wrappedItem = end($storeValue);
4584
                    $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem;
4585 11531
                }
4586
4587 11531
                if (
4588
                    (isset($storeValue) || $tokenData['reference'] === 'NULL')
4589 11531
                    && (!$storeValueAsBool || Information\ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch'))
4590 11531
                ) {
4591 11531
                    // If branching value is not true, we don't need to compute
4592 4
                    if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) {
4593
                        $stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token);
4594 11531
                        $fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true;
4595
                    }
4596 11531
4597 11531
                    if (isset($storeKey)) {
4598 76
                        // We are processing an if condition
4599 76
                        // We cascade the pruning to the depending branches
4600 76
                        $branchStore[$storeKey] = 'Pruned branch';
4601 76
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4602 76
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4603 53
                    }
4604 53
4605
                    continue;
4606
                }
4607
            }
4608 76
4609 76
            if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) {
4610
                $onlyIfNotStoreKey = $tokenData['onlyIfNot'];
4611
                $storeValue = $branchStore[$onlyIfNotStoreKey] ?? null;
4612 53
                $storeValueAsBool = ($storeValue === null)
4613 51
                    ? true : (bool) Functions::flattenSingleValue($storeValue);
4614 51
                if (is_array($storeValue)) {
4615
                    $wrappedItem = end($storeValue);
4616
                    $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem;
4617 53
                }
4618
4619
                if (
4620 1
                    (isset($storeValue) || $tokenData['reference'] === 'NULL')
4621 1
                    && ($storeValueAsBool || Information\ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch'))
4622 1
                ) {
4623
                    // If branching value is true, we don't need to compute
4624
                    if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) {
4625 53
                        $stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token);
4626
                        $fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true;
4627
                    }
4628
4629 11531
                    if (isset($storeKey)) {
4630 71
                        // We are processing an if condition
4631 71
                        // We cascade the pruning to the depending branches
4632 71
                        $branchStore[$storeKey] = 'Pruned branch';
4633 71
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4634 71
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4635 48
                    }
4636 48
4637
                    continue;
4638
                }
4639
            }
4640 71
4641 71
            if ($token instanceof Operands\StructuredReference) {
4642
                if ($cell === null) {
4643
                    return $this->raiseFormulaError('Structured References must exist in a Cell context');
4644 53
                }
4645 53
4646 53
                try {
4647
                    $cellRange = $token->parse($cell);
4648
                    if (str_contains($cellRange, ':')) {
4649 53
                        $this->debugLog->writeDebugLog('Evaluating Structured Reference %s as Cell Range %s', $token->value(), $cellRange);
4650
                        $rangeValue = self::getInstance($cell->getWorksheet()->getParent())->_calculateFormulaValue("={$cellRange}", $cellRange, $cell);
4651
                        $stack->push('Value', $rangeValue);
4652 7
                        $this->debugLog->writeDebugLog('Evaluated Structured Reference %s as value %s', $token->value(), $this->showValue($rangeValue));
4653 7
                    } else {
4654 7
                        $this->debugLog->writeDebugLog('Evaluating Structured Reference %s as Cell %s', $token->value(), $cellRange);
4655
                        $cellValue = $cell->getWorksheet()->getCell($cellRange)->getCalculatedValue(false);
4656
                        $stack->push('Cell Reference', $cellValue, $cellRange);
4657 53
                        $this->debugLog->writeDebugLog('Evaluated Structured Reference %s as value %s', $token->value(), $this->showValue($cellValue));
4658
                    }
4659
                } catch (Exception $e) {
4660
                    if ($e->getCode() === Exception::CALCULATION_ENGINE_PUSH_TO_STACK) {
4661 11531
                        $stack->push('Error', ExcelError::REF(), null);
4662 16
                        $this->debugLog->writeDebugLog('Evaluated Structured Reference %s as error value %s', $token->value(), ExcelError::REF());
4663
                    } else {
4664
                        return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
4665
                    }
4666
                }
4667 16
            } elseif (!is_numeric($token) && !is_object($token) && isset(self::BINARY_OPERATORS[$token])) {
4668 16
                // 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
4669 7
                //    We must have two operands, error if we don't
4670 7
                $operand2Data = $stack->pop();
4671 7
                if ($operand2Data === null) {
4672 7
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4673
                }
4674 10
                $operand1Data = $stack->pop();
4675 10
                if ($operand1Data === null) {
4676 10
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4677 16
                }
4678
4679 2
                $operand1 = self::dataTestReference($operand1Data);
4680 2
                $operand2 = self::dataTestReference($operand2Data);
4681 2
4682 2
                //    Log what we're doing
4683
                if ($token == ':') {
4684
                    $this->debugLog->writeDebugLog('Evaluating Range %s %s %s', $this->showValue($operand1Data['reference']), $token, $this->showValue($operand2Data['reference']));
4685
                } else {
4686
                    $this->debugLog->writeDebugLog('Evaluating %s %s %s', $this->showValue($operand1), $token, $this->showValue($operand2));
4687 11530
                }
4688
4689
                //    Process the operation in the appropriate manner
4690 1550
                switch ($token) {
4691 1550
                    // Comparison (Boolean) Operators
4692
                    case '>': // Greater than
4693
                    case '<': // Less than
4694 1550
                    case '>=': // Greater than or Equal to
4695 1550
                    case '<=': // Less than or Equal to
4696
                    case '=': // Equality
4697
                    case '<>': // Inequality
4698
                        $result = $this->executeBinaryComparisonOperation($operand1, $operand2, (string) $token, $stack);
4699 1550
                        if (isset($storeKey)) {
4700 1550
                            $branchStore[$storeKey] = $result;
4701
                        }
4702
4703 1550
                        break;
4704 1095
                    // Binary Operators
4705
                    case ':': // Range
4706 701
                        if ($operand1Data['type'] === 'Defined Name') {
4707
                            if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false && $this->spreadsheet !== null) {
4708
                                $definedName = $this->spreadsheet->getNamedRange($operand1Data['reference']);
4709
                                if ($definedName !== null) {
4710
                                    $operand1Data['reference'] = $operand1Data['value'] = str_replace('$', '', $definedName->getValue());
4711
                                }
4712 1550
                            }
4713 1537
                        }
4714 1519
                        if (str_contains($operand1Data['reference'] ?? '', '!')) {
4715 1511
                            [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
4716 1493
                        } else {
4717 1343
                            $sheet1 = ($pCellWorksheet !== null) ? $pCellWorksheet->getTitle() : '';
4718 396
                        }
4719 396
                        $sheet1 ??= '';
4720 63
4721
                        [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true);
4722
                        if (empty($sheet2)) {
4723 396
                            $sheet2 = $sheet1;
4724
                        }
4725 1333
4726 1095
                        if (trim($sheet1, "'") === trim($sheet2, "'")) {
4727 3
                            if ($operand1Data['reference'] === null && $cell !== null) {
4728 3
                                if (is_array($operand1Data['value'])) {
4729 3
                                    $operand1Data['reference'] = $cell->getCoordinate();
4730 3
                                } elseif ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
4731
                                    $operand1Data['reference'] = $cell->getColumn() . $operand1Data['value'];
4732
                                } elseif (trim($operand1Data['value']) == '') {
4733
                                    $operand1Data['reference'] = $cell->getCoordinate();
4734 1095
                                } else {
4735 1089
                                    $operand1Data['reference'] = $operand1Data['value'] . $cell->getRow();
4736
                                }
4737 10
                            }
4738
                            if ($operand2Data['reference'] === null && $cell !== null) {
4739 1095
                                if (is_array($operand2Data['value'])) {
4740
                                    $operand2Data['reference'] = $cell->getCoordinate();
4741 1095
                                } elseif ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
4742 1095
                                    $operand2Data['reference'] = $cell->getColumn() . $operand2Data['value'];
4743 4
                                } elseif (trim($operand2Data['value']) == '') {
4744
                                    $operand2Data['reference'] = $cell->getCoordinate();
4745
                                } else {
4746 1095
                                    $operand2Data['reference'] = $operand2Data['value'] . $cell->getRow();
4747 1092
                                }
4748
                            }
4749
4750
                            $oData = array_merge(explode(':', $operand1Data['reference'] ?? ''), explode(':', $operand2Data['reference'] ?? ''));
4751
                            $oCol = $oRow = [];
4752
                            $breakNeeded = false;
4753
                            foreach ($oData as $oDatum) {
4754
                                try {
4755
                                    $oCR = Coordinate::coordinateFromString($oDatum);
4756
                                    $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
4757
                                    $oRow[] = $oCR[1];
4758 1092
                                } catch (\Exception) {
4759 2
                                    $stack->push('Error', ExcelError::REF(), null);
4760 1
                                    $breakNeeded = true;
4761 1
4762
                                    break;
4763 1
                                }
4764
                            }
4765
                            if ($breakNeeded) {
4766 1
                                break;
4767
                            }
4768
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4769
                            if ($pCellParent !== null && $this->spreadsheet !== null) {
4770 1092
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
4771 1092
                            } else {
4772 1092
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4773 1092
                            }
4774
4775 1092
                            $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellValue));
4776 1092
                            $stack->push('Cell Reference', $cellValue, $cellRef);
4777 1092
                        } else {
4778 1
                            $this->debugLog->writeDebugLog('Evaluation Result is a #REF! Error');
4779 1
                            $stack->push('Error', ExcelError::REF(), null);
4780 1
                        }
4781
4782 1
                        break;
4783
                    case '+':            //    Addition
4784
                    case '-':            //    Subtraction
4785 1092
                    case '*':            //    Multiplication
4786 1
                    case '/':            //    Division
4787
                    case '^':            //    Exponential
4788 1091
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, $stack);
4789 1091
                        if (isset($storeKey)) {
4790 1091
                            $branchStore[$storeKey] = $result;
4791
                        }
4792
4793
                        break;
4794
                    case '&':            //    Concatenation
4795 1091
                        //    If either of the operands is a matrix, we need to treat them both as matrices
4796 1091
                        //        (converting the other operand to a matrix if need be); then perform the required
4797
                        //        matrix operation
4798 4
                        $operand1 = self::boolToString($operand1);
4799 4
                        $operand2 = self::boolToString($operand2);
4800
                        if (is_array($operand1) || is_array($operand2)) {
4801
                            if (is_string($operand1)) {
4802 1094
                                $operand1 = self::unwrapResult($operand1);
4803 361
                            }
4804 285
                            if (is_string($operand2)) {
4805 246
                                $operand2 = self::unwrapResult($operand2);
4806 126
                            }
4807 36
                            //    Ensure that both operands are arrays/matrices
4808 336
                            [$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 2);
4809 336
4810 5
                            for ($row = 0; $row < $rows; ++$row) {
4811
                                for ($column = 0; $column < $columns; ++$column) {
4812
                                    $op1x = self::boolToString($operand1[$row][$column]);
4813 336
                                    $op2x = self::boolToString($operand2[$row][$column]);
4814 33
                                    if (Information\ErrorValue::isError($op1x)) {
4815
                                        // no need to do anything
4816
                                    } elseif (Information\ErrorValue::isError($op2x)) {
4817
                                        $operand1[$row][$column] = $op2x;
4818 19
                                    } else {
4819 19
                                        $operand1[$row][$column]
4820 19
                                            = Shared\StringHelper::substring(
4821 15
                                                $op1x . $op2x,
4822 6
                                                0,
4823
                                                DataType::MAX_STRING_LENGTH
4824 15
                                            );
4825 5
                                    }
4826
                                }
4827
                            }
4828 15
                            $result = $operand1;
4829
                        } else {
4830 15
                            // In theory, we should truncate here.
4831 15
                            // But I can't figure out a formula
4832 15
                            // using the concatenation operator
4833 15
                            // with literals that fits in 32K,
4834 15
                            // so I don't think we can overflow here.
4835
                            if (Information\ErrorValue::isError($operand1)) {
4836 15
                                $result = $operand1;
4837 1
                            } elseif (Information\ErrorValue::isError($operand2)) {
4838
                                $result = $operand2;
4839 14
                            } else {
4840 14
                                $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE;
4841 14
                            }
4842 14
                        }
4843 14
                        $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result));
4844 14
                        $stack->push('Value', $result);
4845
4846
                        if (isset($storeKey)) {
4847
                            $branchStore[$storeKey] = $result;
4848 15
                        }
4849
4850
                        break;
4851
                    case '∩':            //    Intersect
4852
                        $rowIntersect = array_intersect_key($operand1, $operand2);
4853
                        $cellIntersect = $oCol = $oRow = [];
4854
                        foreach (array_keys($rowIntersect) as $row) {
4855 6
                            $oRow[] = $row;
4856
                            foreach ($rowIntersect[$row] as $col => $data) {
4857 6
                                $oCol[] = Coordinate::columnIndexFromString($col) - 1;
4858
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
4859
                            }
4860 6
                        }
4861
                        if (count(Functions::flattenArray($cellIntersect)) === 0) {
4862
                            $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
4863 19
                            $stack->push('Error', ExcelError::null(), null);
4864 19
                        } else {
4865
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':'
4866 19
                                . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4867
                            $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
4868
                            $stack->push('Value', $cellIntersect, $cellRef);
4869
                        }
4870 19
4871 14
                        break;
4872 14
                }
4873 14
            } elseif (($token === '~') || ($token === '%')) {
4874 14
                // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
4875 14
                if (($arg = $stack->pop()) === null) {
4876 14
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4877 14
                }
4878 14
                $arg = $arg['value'];
4879
                if ($token === '~') {
4880
                    $this->debugLog->writeDebugLog('Evaluating Negation of %s', $this->showValue($arg));
4881 14
                    $multiplier = -1;
4882 2
                } else {
4883 2
                    $this->debugLog->writeDebugLog('Evaluating Percentile of %s', $this->showValue($arg));
4884
                    $multiplier = 0.01;
4885 12
                }
4886 12
                if (is_array($arg)) {
4887 12
                    $operand2 = $multiplier;
4888 12
                    $result = $arg;
4889
                    [$rows, $columns] = self::checkMatrixOperands($result, $operand2, 0);
4890
                    for ($row = 0; $row < $rows; ++$row) {
4891 14
                        for ($column = 0; $column < $columns; ++$column) {
4892
                            if (self::isNumericOrBool($result[$row][$column])) {
4893 11523
                                $result[$row][$column] *= $multiplier;
4894
                            } else {
4895 1109
                                $result[$row][$column] = self::makeError($result[$row][$column]);
4896
                            }
4897
                        }
4898 1109
                    }
4899 1109
4900 1106
                    $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result));
4901 1106
                    $stack->push('Value', $result);
4902
                    if (isset($storeKey)) {
4903 5
                        $branchStore[$storeKey] = $result;
4904 5
                    }
4905
                } else {
4906 1109
                    $this->executeNumericBinaryOperation($multiplier, $arg, '*', $stack);
4907 4
                }
4908 4
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token ?? '', $matches)) {
4909 4
                $cellRef = null;
4910 4
4911 4
                if (isset($matches[8])) {
4912 4
                    if ($cell === null) {
4913 4
                        // We can't access the range, so return a REF error
4914
                        $cellValue = ExcelError::REF();
4915 2
                    } else {
4916
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
4917
                        if ($matches[2] > '') {
4918
                            $matches[2] = trim($matches[2], "\"'");
4919
                            if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
4920 4
                                //    It's a Reference to an external spreadsheet (not currently supported)
4921 4
                                return $this->raiseFormulaError('Unable to access External Workbook');
4922 4
                            }
4923
                            $matches[2] = trim($matches[2], "\"'");
4924
                            $this->debugLog->writeDebugLog('Evaluating Cell Range %s in worksheet %s', $cellRef, $matches[2]);
4925
                            if ($pCellParent !== null && $this->spreadsheet !== null) {
4926 1108
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4927
                            } else {
4928 11523
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4929 6757
                            }
4930
                            $this->debugLog->writeDebugLog('Evaluation Result for cells %s in worksheet %s is %s', $cellRef, $matches[2], $this->showTypeDetails($cellValue));
4931 6757
                        } else {
4932
                            $this->debugLog->writeDebugLog('Evaluating Cell Range %s in current worksheet', $cellRef);
4933
                            if ($pCellParent !== null) {
4934
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4935
                            } else {
4936
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4937
                            }
4938
                            $this->debugLog->writeDebugLog('Evaluation Result for cells %s is %s', $cellRef, $this->showTypeDetails($cellValue));
4939
                        }
4940
                    }
4941
                } else {
4942
                    if ($cell === null) {
4943
                        // We can't access the cell, so return a REF error
4944
                        $cellValue = ExcelError::REF();
4945
                    } else {
4946
                        $cellRef = $matches[6] . $matches[7];
4947
                        if ($matches[2] > '') {
4948
                            $matches[2] = trim($matches[2], "\"'");
4949
                            if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
4950
                                //    It's a Reference to an external spreadsheet (not currently supported)
4951
                                return $this->raiseFormulaError('Unable to access External Workbook');
4952
                            }
4953
                            $this->debugLog->writeDebugLog('Evaluating Cell %s in worksheet %s', $cellRef, $matches[2]);
4954
                            if ($pCellParent !== null && $this->spreadsheet !== null) {
4955
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
4956
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
4957
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4958
                                    $cell->attach($pCellParent);
4959
                                } else {
4960
                                    $cellRef = ($cellSheet !== null) ? "'{$matches[2]}'!{$cellRef}" : $cellRef;
4961
                                    $cellValue = ($cellSheet !== null) ? null : ExcelError::REF();
4962 6757
                                }
4963
                            } else {
4964
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4965
                            }
4966 6757
                            $this->debugLog->writeDebugLog('Evaluation Result for cell %s in worksheet %s is %s', $cellRef, $matches[2], $this->showTypeDetails($cellValue));
4967 6757
                        } else {
4968 6752
                            $this->debugLog->writeDebugLog('Evaluating Cell %s in current worksheet', $cellRef);
4969 6752
                            if ($pCellParent !== null && $pCellParent->has($cellRef)) {
4970
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4971 1
                                $cell->attach($pCellParent);
4972
                            } else {
4973 6752
                                $cellValue = null;
4974 6752
                            }
4975 6752
                            $this->debugLog->writeDebugLog('Evaluation Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue));
4976 6752
                        }
4977 6639
                    }
4978 6639
                }
4979
4980 323
                if ($this->getInstanceArrayReturnType() === self::RETURN_ARRAY_AS_ARRAY && !$this->processingAnchorArray && is_array($cellValue)) {
4981 323
                    while (is_array($cellValue)) {
4982
                        $cellValue = array_shift($cellValue);
4983
                    }
4984
                    $this->debugLog->writeDebugLog('Scalar Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue));
4985
                }
4986 6752
                $this->processingAnchorArray = false;
4987
                $stack->push('Cell Value', $cellValue, $cellRef);
4988 9
                if (isset($storeKey)) {
4989 9
                    $branchStore[$storeKey] = $cellValue;
4990 9
                }
4991 9
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) {
4992
                // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
4993 2
                if ($cell !== null && $pCellParent !== null) {
4994
                    $cell->attach($pCellParent);
4995 9
                }
4996
4997
                $functionName = $matches[1];
4998
                $argCount = $stack->pop();
4999
                $argCount = $argCount['value'];
5000 6757
                if ($functionName !== 'MKMATRIX') {
5001 50
                    $this->debugLog->writeDebugLog('Evaluating Function %s() with %s argument%s', self::localeFunc($functionName), (($argCount == 0) ? 'no' : $argCount), (($argCount == 1) ? '' : 's'));
5002 50
                }
5003
                if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) {    // function
5004 50
                    $passByReference = false;
5005
                    $passCellReference = false;
5006 6757
                    $functionCall = null;
5007 6757
                    if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
5008 6757
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
5009 55
                        $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
5010
                        $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
5011 11453
                    } elseif (isset(self::$controlFunctions[$functionName])) {
5012
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
5013 11257
                        $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
5014 7687
                        $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
5015
                    }
5016
5017 11257
                    // get the arguments for this function
5018 11257
                    $args = $argArrayVals = [];
5019 11257
                    $emptyArguments = [];
5020 11257
                    for ($i = 0; $i < $argCount; ++$i) {
5021 11256
                        $arg = $stack->pop();
5022
                        $a = $argCount - $i - 1;
5023 11257
                        if (
5024 11257
                            ($passByReference)
5025 11257
                            && (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a]))
5026 11257
                            && (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])
5027 11257
                        ) {
5028 11254
                            if ($arg['reference'] === null) {
5029 11254
                                $nextArg = $cellID;
5030 11254
                                if ($functionName === 'ISREF' && is_array($arg) && ($arg['type'] ?? '') === 'Value') {
5031 799
                                    if (array_key_exists('value', $arg)) {
5032 799
                                        $argValue = $arg['value'];
5033 799
                                        if (is_scalar($argValue)) {
5034 799
                                            $nextArg = $argValue;
5035
                                        } elseif (empty($argValue)) {
5036
                                            $nextArg = '';
5037
                                        }
5038 11257
                                    }
5039 11257
                                }
5040 11257
                                $args[] = $nextArg;
5041 11239
                                if ($functionName !== 'MKMATRIX') {
5042 11239
                                    $argArrayVals[] = $this->showValue($cellID);
5043
                                }
5044 11239
                            } else {
5045 11239
                                $args[] = $arg['reference'];
5046 11239
                                if ($functionName !== 'MKMATRIX') {
5047
                                    $argArrayVals[] = $this->showValue($arg['reference']);
5048 52
                                }
5049 1
                            }
5050 1
                        } else {
5051
                            if ($arg['type'] === 'Empty Argument' && in_array($functionName, ['MIN', 'MINA', 'MAX', 'MAXA', 'IF'], true)) {
5052
                                $emptyArguments[] = false;
5053
                                $args[] = $arg['value'] = 0;
5054
                                $this->debugLog->writeDebugLog('Empty Argument reevaluated as 0');
5055
                            } else {
5056
                                $emptyArguments[] = $arg['type'] === 'Empty Argument';
5057
                                $args[] = self::unwrapResult($arg['value']);
5058
                            }
5059
                            if ($functionName !== 'MKMATRIX') {
5060 1
                                $argArrayVals[] = $this->showValue($arg['value']);
5061 1
                            }
5062 1
                        }
5063
                    }
5064
5065 51
                    //    Reverse the order of the arguments
5066 51
                    krsort($args);
5067 51
                    krsort($emptyArguments);
5068
5069
                    if ($argCount > 0 && is_array($functionCall)) {
5070
                        $args = $this->addDefaultArgumentValues($functionCall, $args, $emptyArguments);
5071 11206
                    }
5072 15
5073 15
                    if (($passByReference) && ($argCount == 0)) {
5074 15
                        $args[] = $cellID;
5075
                        $argArrayVals[] = $this->showValue($cellID);
5076 11206
                    }
5077 11206
5078
                    if ($functionName !== 'MKMATRIX') {
5079 11206
                        if ($this->debugLog->getWriteDebugLog()) {
5080 11205
                            krsort($argArrayVals);
5081
                            $this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)));
5082
                        }
5083
                    }
5084
5085
                    //    Process the argument with the appropriate function call
5086 11257
                    if ($pCellWorksheet !== null && $originalCoordinate !== null) {
5087 11257
                        $pCellWorksheet->getCell($originalCoordinate);
5088
                    }
5089 11257
                    $args = $this->addCellReference($args, $passCellReference, $functionCall, $cell);
5090 11239
5091
                    if (!is_array($functionCall)) {
5092
                        foreach ($args as &$arg) {
5093 11257
                            $arg = Functions::flattenSingleValue($arg);
5094 9
                        }
5095 9
                        unset($arg);
5096
                    }
5097
5098 11257
                    $result = call_user_func_array($functionCall, $args);
5099 11256
5100 2
                    if ($functionName !== 'MKMATRIX') {
5101 2
                        $this->debugLog->writeDebugLog('Evaluation Result for %s() function call is %s', self::localeFunc($functionName), $this->showTypeDetails($result));
5102
                    }
5103
                    $stack->push('Value', self::wrapResult($result));
5104
                    if (isset($storeKey)) {
5105
                        $branchStore[$storeKey] = $result;
5106 11257
                    }
5107 7687
                }
5108
            } else {
5109 11257
                // if the token is a number, boolean, string or an Excel error, push it onto the stack
5110
                if (isset(self::$excelConstants[strtoupper($token ?? '')])) {
5111 11257
                    $excelConstant = strtoupper($token);
5112 53
                    $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
5113
                    if (isset($storeKey)) {
5114
                        $branchStore[$storeKey] = self::$excelConstants[$excelConstant];
5115 53
                    }
5116
                    $this->debugLog->writeDebugLog('Evaluating Constant %s as %s', $excelConstant, $this->showTypeDetails(self::$excelConstants[$excelConstant]));
5117
                } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) {
5118 11257
                    $stack->push($tokenData['type'], $token, $tokenData['reference']);
5119
                    if (isset($storeKey)) {
5120 11251
                        $branchStore[$storeKey] = $token;
5121 11248
                    }
5122
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token, $matches)) {
5123 11251
                    // if the token is a named range or formula, evaluate it and push the result onto the stack
5124 11251
                    $definedName = $matches[6];
5125 19
                    if ($cell === null || $pCellWorksheet === null) {
5126
                        return $this->raiseFormulaError("undefined name '$token'");
5127
                    }
5128
                    $specifiedWorksheet = trim($matches[2], "'");
5129
5130 11453
                    $this->debugLog->writeDebugLog('Evaluating Defined Name %s', $definedName);
5131
                    $namedRange = DefinedName::resolveName($definedName, $pCellWorksheet, $specifiedWorksheet);
5132
                    // If not Defined Name, try as Table.
5133
                    if ($namedRange === null && $this->spreadsheet !== null) {
5134
                        $table = $this->spreadsheet->getTableByName($definedName);
5135
                        if ($table !== null) {
5136
                            $tableRange = Coordinate::getRangeBoundaries($table->getRange());
5137 11453
                            if ($table->getShowHeaderRow()) {
5138 11408
                                ++$tableRange[0][1];
5139 11408
                            }
5140 66
                            if ($table->getShowTotalsRow()) {
5141
                                --$tableRange[1][1];
5142 131
                            }
5143
                            $tableRangeString
5144 131
                                = '$' . $tableRange[0][0]
5145 131
                                . '$' . $tableRange[0][1]
5146
                                . ':'
5147
                                . '$' . $tableRange[1][0]
5148 131
                                . '$' . $tableRange[1][1];
5149
                            $namedRange = new NamedRange($definedName, $table->getWorksheet(), $tableRangeString);
5150 131
                        }
5151 131
                    }
5152
                    if ($namedRange === null) {
5153 131
                        return $this->raiseFormulaError("undefined name '$definedName'");
5154 33
                    }
5155 33
5156 3
                    $result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== '');
5157 3
                    if (isset($storeKey)) {
5158 3
                        $branchStore[$storeKey] = $result;
5159
                    }
5160 3
                } else {
5161
                    return $this->raiseFormulaError("undefined name '$token'");
5162
                }
5163 3
            }
5164 3
        }
5165 3
        // when we're out of tokens, the stack should have a single element, the final result
5166 3
        if ($stack->count() != 1) {
5167 3
            return $this->raiseFormulaError('internal error');
5168 3
        }
5169 3
        $output = $stack->pop();
5170
        $output = $output['value'];
5171
5172 131
        return $output;
5173 30
    }
5174
5175
    private function validateBinaryOperand(mixed &$operand, mixed &$stack): bool
5176 111
    {
5177 111
        if (is_array($operand)) {
5178 1
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
5179
                do {
5180
                    $operand = array_pop($operand);
5181
                } while (is_array($operand));
5182
            }
5183
        }
5184
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
5185
        if (is_string($operand)) {
5186 11504
            //    We only need special validations for the operand if it is a string
5187 1
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
5188
            if ($operand > '' && $operand[0] == self::FORMULA_STRING_QUOTE) {
5189 11504
                $operand = self::unwrapResult($operand);
5190 11504
            }
5191
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
5192 11504
            if (!is_numeric($operand)) {
5193
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
5194
                if ($operand > '' && $operand[0] == '#') {
5195 1381
                    $stack->push('Value', $operand);
5196
                    $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($operand));
5197 1381
5198 213
                    return false;
5199
                } elseif (Engine\FormattedNumber::convertToNumberIfFormatted($operand) === false) {
5200 181
                    //    If not a numeric, a fraction or a percentage, then it's a text string, and so can't be used in mathematical binary operations
5201 181
                    $stack->push('Error', '#VALUE!');
5202
                    $this->debugLog->writeDebugLog('Evaluation Result is a %s', $this->showTypeDetails('#VALUE!'));
5203
5204
                    return false;
5205 1381
                }
5206
            }
5207
        }
5208 13
5209 5
        //    return a true if the value of the operand is one that we can use in normal binary mathematical operations
5210
        return true;
5211
    }
5212 13
5213
    private function executeArrayComparison(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays): array
5214 12
    {
5215 4
        $result = [];
5216 4
        if (!is_array($operand2)) {
5217
            // Operand 1 is an array, Operand 2 is a scalar
5218 4
            foreach ($operand1 as $x => $operandData) {
5219 8
                $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operandData), $operation, $this->showValue($operand2));
5220
                $this->executeBinaryComparisonOperation($operandData, $operand2, $operation, $stack);
5221 4
                $r = $stack->pop();
5222 4
                $result[$x] = $r['value'];
5223
            }
5224 4
        } elseif (!is_array($operand1)) {
5225
            // Operand 1 is a scalar, Operand 2 is an array
5226
            foreach ($operand2 as $x => $operandData) {
5227
                $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operand1), $operation, $this->showValue($operandData));
5228
                $this->executeBinaryComparisonOperation($operand1, $operandData, $operation, $stack);
5229
                $r = $stack->pop();
5230 1379
                $result[$x] = $r['value'];
5231
            }
5232
        } else {
5233 50
            // Operand 1 and Operand 2 are both arrays
5234
            if (!$recursingArrays) {
5235 50
                self::checkMatrixOperands($operand1, $operand2, 2);
5236 50
            }
5237
            foreach ($operand1 as $x => $operandData) {
5238 47
                $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operandData), $operation, $this->showValue($operand2[$x]));
5239 47
                $this->executeBinaryComparisonOperation($operandData, $operand2[$x], $operation, $stack, true);
5240 47
                $r = $stack->pop();
5241 47
                $result[$x] = $r['value'];
5242 47
            }
5243
        }
5244 7
        //    Log the result details
5245
        $this->debugLog->writeDebugLog('Comparison Evaluation Result is %s', $this->showTypeDetails($result));
5246 3
        //    And push the result onto the stack
5247 3
        $stack->push('Array', $result);
5248 3
5249 3
        return $result;
5250 3
    }
5251
5252
    private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool
5253
    {
5254 6
        //    If we're dealing with matrix operations, we want a matrix result
5255 6
        if ((is_array($operand1)) || (is_array($operand2))) {
5256
            return $this->executeArrayComparison($operand1, $operand2, $operation, $stack, $recursingArrays);
5257 6
        }
5258 6
5259 6
        $result = BinaryComparison::compare($operand1, $operand2, $operation);
5260 6
5261 6
        //    Log the result details
5262
        $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result));
5263
        //    And push the result onto the stack
5264
        $stack->push('Value', $result);
5265 50
5266
        return $result;
5267 50
    }
5268
5269 50
    private function executeNumericBinaryOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack): mixed
5270
    {
5271
        //    Validate the two operands
5272 396
        if (
5273
            ($this->validateBinaryOperand($operand1, $stack) === false)
5274
            || ($this->validateBinaryOperand($operand2, $stack) === false)
5275 396
        ) {
5276 50
            return false;
5277
        }
5278
5279 396
        if (
5280
            (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE)
5281
            && ((is_string($operand1) && !is_numeric($operand1) && $operand1 !== '')
5282 396
                || (is_string($operand2) && !is_numeric($operand2) && $operand2 !== ''))
5283
        ) {
5284 396
            $result = ExcelError::VALUE();
5285
        } elseif (is_array($operand1) || is_array($operand2)) {
5286 396
            //    Ensure that both operands are arrays/matrices
5287
            if (is_array($operand1)) {
5288
                foreach ($operand1 as $key => $value) {
5289 1381
                    $operand1[$key] = Functions::flattenArray($value);
5290
                }
5291
            }
5292
            if (is_array($operand2)) {
5293 1381
                foreach ($operand2 as $key => $value) {
5294 1381
                    $operand2[$key] = Functions::flattenArray($value);
5295
                }
5296 8
            }
5297
            [$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 3);
5298
5299
            for ($row = 0; $row < $rows; ++$row) {
5300 1375
                for ($column = 0; $column < $columns; ++$column) {
5301 1375
                    if ($operand1[$row][$column] === null) {
5302 1375
                        $operand1[$row][$column] = 0;
5303
                    } elseif (!self::isNumericOrBool($operand1[$row][$column])) {
5304
                        $operand1[$row][$column] = self::makeError($operand1[$row][$column]);
5305 1375
5306
                        continue;
5307 34
                    }
5308 28
                    if ($operand2[$row][$column] === null) {
5309 28
                        $operand2[$row][$column] = 0;
5310
                    } elseif (!self::isNumericOrBool($operand2[$row][$column])) {
5311
                        $operand1[$row][$column] = self::makeError($operand2[$row][$column]);
5312 34
5313 28
                        continue;
5314 28
                    }
5315
                    switch ($operation) {
5316
                        case '+':
5317 34
                            $operand1[$row][$column] += $operand2[$row][$column];
5318
5319 34
                            break;
5320 34
                        case '-':
5321 34
                            $operand1[$row][$column] -= $operand2[$row][$column];
5322 1
5323 34
                            break;
5324 1
                        case '*':
5325
                            $operand1[$row][$column] *= $operand2[$row][$column];
5326 1
5327
                            break;
5328 34
                        case '/':
5329 1
                            if ($operand2[$row][$column] == 0) {
5330 34
                                $operand1[$row][$column] = ExcelError::DIV0();
5331
                            } else {
5332
                                $operand1[$row][$column] /= $operand2[$row][$column];
5333
                            }
5334
5335
                            break;
5336 34
                        case '^':
5337 3
                            $operand1[$row][$column] = $operand1[$row][$column] ** $operand2[$row][$column];
5338
5339 3
                            break;
5340 31
5341 3
                        default:
5342
                            throw new Exception('Unsupported numeric binary operation');
5343 3
                    }
5344 29
                }
5345 22
            }
5346
            $result = $operand1;
5347 22
        } else {
5348 7
            //    If we're dealing with non-matrix operations, execute the necessary operation
5349 5
            switch ($operation) {
5350 3
                //    Addition
5351
                case '+':
5352 4
                    $result = $operand1 + $operand2;
5353
5354
                    break;
5355 5
                //    Subtraction
5356 2
                case '-':
5357 2
                    $result = $operand1 - $operand2;
5358
5359 2
                    break;
5360
                //    Multiplication
5361
                case '*':
5362
                    $result = $operand1 * $operand2;
5363
5364
                    break;
5365
                //    Division
5366 34
                case '/':
5367
                    if ($operand2 == 0) {
5368
                        //    Trap for Divide by Zero error
5369
                        $stack->push('Error', ExcelError::DIV0());
5370
                        $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails(ExcelError::DIV0()));
5371 1360
5372 151
                        return false;
5373
                    }
5374 151
                    $result = $operand1 / $operand2;
5375
5376 1290
                    break;
5377 50
                //    Power
5378
                case '^':
5379 50
                    $result = $operand1 ** $operand2;
5380
5381 1256
                    break;
5382 1198
5383
                default:
5384 1198
                    throw new Exception('Unsupported numeric binary operation');
5385
            }
5386 90
        }
5387 89
5388
        //    Log the result details
5389 41
        $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result));
5390 41
        //    And push the result onto the stack
5391
        $stack->push('Value', $result);
5392 41
5393
        return $result;
5394 58
    }
5395
5396 58
    /**
5397
     * Trigger an error, but nicely, if need be.
5398 2
     *
5399 2
     * @return false
5400
     */
5401 2
    protected function raiseFormulaError(string $errorMessage, int $code = 0, ?Throwable $exception = null): bool
5402
    {
5403
        $this->formulaError = $errorMessage;
5404
        $this->cyclicReferenceStack->clear();
5405
        $suppress = $this->suppressFormulaErrors;
5406
        if (!$suppress) {
5407
            throw new Exception($errorMessage, $code, $exception);
5408
        }
5409 1349
5410
        return false;
5411 1349
    }
5412
5413 1349
    /**
5414
     * Extract range values.
5415
     *
5416
     * @param string $range String based range representation
5417
     * @param ?Worksheet $worksheet Worksheet
5418
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
5419
     *
5420
     * @return array Array of values in range if range contains more than one element. Otherwise, a single value is returned.
5421 263
     */
5422
    public function extractCellRange(string &$range = 'A1', ?Worksheet $worksheet = null, bool $resetLog = true): array
5423 263
    {
5424 263
        // Return value
5425 263
        $returnValue = [];
5426 263
5427 262
        if ($worksheet !== null) {
5428
            $worksheetName = $worksheet->getTitle();
5429
5430 2
            if (str_contains($range, '!')) {
5431
                [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true);
5432
                $worksheet = ($this->spreadsheet === null) ? null : $this->spreadsheet->getSheetByName($worksheetName);
5433
            }
5434
5435
            // Extract range
5436
            $aReferences = Coordinate::extractAllCellReferencesInRange($range);
5437
            $range = "'" . $worksheetName . "'" . '!' . $range;
5438
            $currentCol = '';
5439
            $currentRow = 0;
5440
            if (!isset($aReferences[1])) {
5441
                //    Single cell in range
5442 6719
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
5443
                if ($worksheet !== null && $worksheet->cellExists($aReferences[0])) {
5444
                    $temp = $worksheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5445 6719
                    if ($this->getInstanceArrayReturnType() === self::RETURN_ARRAY_AS_ARRAY) {
5446
                        while (is_array($temp)) {
5447 6719
                            $temp = array_shift($temp);
5448 6719
                        }
5449
                    }
5450 6719
                    $returnValue[$currentRow][$currentCol] = $temp;
5451 10
                } else {
5452 10
                    $returnValue[$currentRow][$currentCol] = null;
5453
                }
5454
            } else {
5455
                // Extract cell data for all cells in the range
5456 6719
                foreach ($aReferences as $reference) {
5457 6719
                    // Extract range
5458 6719
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
5459 6719
                    if ($worksheet !== null && $worksheet->cellExists($reference)) {
5460 6719
                        $temp = $worksheet->getCell($reference)->getCalculatedValue($resetLog);
5461
                        if ($this->getInstanceArrayReturnType() === self::RETURN_ARRAY_AS_ARRAY) {
5462 6687
                            while (is_array($temp)) {
5463 6687
                                $temp = array_shift($temp);
5464 6685
                            }
5465 6685
                        }
5466 52
                        $returnValue[$currentRow][$currentCol] = $temp;
5467 6
                    } else {
5468
                        $returnValue[$currentRow][$currentCol] = null;
5469
                    }
5470 6685
                }
5471
            }
5472 4
        }
5473
5474
        return $returnValue;
5475
    }
5476 1094
5477
    /**
5478 1094
     * Extract range values.
5479 1094
     *
5480 1062
     * @param string $range String based range representation
5481 1062
     * @param null|Worksheet $worksheet Worksheet
5482 36
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
5483
     *
5484
     * @return array|string Array of values in range if range contains more than one element. Otherwise, a single value is returned.
5485
     */
5486 1062
    public function extractNamedRange(string &$range = 'A1', ?Worksheet $worksheet = null, bool $resetLog = true): string|array
5487
    {
5488 159
        // Return value
5489
        $returnValue = [];
5490
5491
        if ($worksheet !== null) {
5492
            if (str_contains($range, '!')) {
5493
                [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true);
5494 6719
                $worksheet = ($this->spreadsheet === null) ? null : $this->spreadsheet->getSheetByName($worksheetName);
5495
            }
5496
5497
            // Named range?
5498
            $namedRange = ($worksheet === null) ? null : DefinedName::resolveName($range, $worksheet);
5499
            if ($namedRange === null) {
5500
                return ExcelError::REF();
5501
            }
5502
5503
            $worksheet = $namedRange->getWorksheet();
5504
            $range = $namedRange->getValue();
5505
            $splitRange = Coordinate::splitRange($range);
5506
            //    Convert row and column references
5507
            if ($worksheet !== null && ctype_alpha($splitRange[0][0])) {
5508
                $range = $splitRange[0][0] . '1:' . $splitRange[0][1] . $worksheet->getHighestRow();
5509
            } elseif ($worksheet !== null && ctype_digit($splitRange[0][0])) {
5510
                $range = 'A' . $splitRange[0][0] . ':' . $worksheet->getHighestColumn() . $splitRange[0][1];
5511
            }
5512
5513
            // Extract range
5514
            $aReferences = Coordinate::extractAllCellReferencesInRange($range);
5515
            if (!isset($aReferences[1])) {
5516
                //    Single cell (or single column or row) in range
5517
                [$currentCol, $currentRow] = Coordinate::coordinateFromString($aReferences[0]);
5518
                if ($worksheet !== null && $worksheet->cellExists($aReferences[0])) {
5519
                    $returnValue[$currentRow][$currentCol] = $worksheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5520
                } else {
5521
                    $returnValue[$currentRow][$currentCol] = null;
5522
                }
5523
            } else {
5524
                // Extract cell data for all cells in the range
5525
                foreach ($aReferences as $reference) {
5526
                    // Extract range
5527
                    [$currentCol, $currentRow] = Coordinate::coordinateFromString($reference);
5528
                    if ($worksheet !== null && $worksheet->cellExists($reference)) {
5529
                        $returnValue[$currentRow][$currentCol] = $worksheet->getCell($reference)->getCalculatedValue($resetLog);
5530
                    } else {
5531
                        $returnValue[$currentRow][$currentCol] = null;
5532
                    }
5533
                }
5534
            }
5535
        }
5536
5537
        return $returnValue;
5538
    }
5539
5540
    /**
5541
     * Is a specific function implemented?
5542
     *
5543
     * @param string $function Function Name
5544
     */
5545
    public function isImplemented(string $function): bool
5546
    {
5547
        $function = strtoupper($function);
5548
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$function]) || (is_array(self::$phpSpreadsheetFunctions[$function]['functionCall']) && self::$phpSpreadsheetFunctions[$function]['functionCall'][1] === 'DUMMY');
5549
5550
        return !$notImplemented;
5551
    }
5552
5553
    /**
5554
     * Get a list of all implemented functions as an array of function objects.
5555
     */
5556
    public static function getFunctions(): array
5557
    {
5558
        return self::$phpSpreadsheetFunctions;
5559
    }
5560
5561
    /**
5562
     * Get a list of implemented Excel function names.
5563
     */
5564
    public function getImplementedFunctionNames(): array
5565 3
    {
5566
        $returnValue = [];
5567 3
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
5568 3
            if ($this->isImplemented($functionName)) {
5569
                $returnValue[] = $functionName;
5570 3
            }
5571
        }
5572
5573
        return $returnValue;
5574
    }
5575
5576 2
    private function addDefaultArgumentValues(array $functionCall, array $args, array $emptyArguments): array
5577
    {
5578 2
        $reflector = new ReflectionMethod($functionCall[0], $functionCall[1]);
5579
        $methodArguments = $reflector->getParameters();
5580
5581
        if (count($methodArguments) > 0) {
5582
            // Apply any defaults for empty argument values
5583
            foreach ($emptyArguments as $argumentId => $isArgumentEmpty) {
5584 2
                if ($isArgumentEmpty === true) {
5585
                    $reflectedArgumentId = count($args) - (int) $argumentId - 1;
5586 2
                    if (
5587 2
                        !array_key_exists($reflectedArgumentId, $methodArguments)
5588 2
                        || $methodArguments[$reflectedArgumentId]->isVariadic()
5589 2
                    ) {
5590
                        break;
5591
                    }
5592
5593 2
                    $args[$argumentId] = $this->getArgumentDefaultValue($methodArguments[$reflectedArgumentId]);
5594
                }
5595
            }
5596 11239
        }
5597
5598 11239
        return $args;
5599 11239
    }
5600
5601 11239
    private function getArgumentDefaultValue(ReflectionParameter $methodArgument): mixed
5602
    {
5603 11231
        $defaultValue = null;
5604 11198
5605 134
        if ($methodArgument->isDefaultValueAvailable()) {
5606
            $defaultValue = $methodArgument->getDefaultValue();
5607 134
            if ($methodArgument->isDefaultValueConstant()) {
5608 134
                $constantName = $methodArgument->getDefaultValueConstantName() ?? '';
5609
                // read constant value
5610 12
                if (str_contains($constantName, '::')) {
5611
                    [$className, $constantName] = explode('::', $constantName);
5612
                    $constantReflector = new ReflectionClassConstant($className, $constantName);
5613 122
5614
                    return $constantReflector->getValue();
5615
                }
5616
5617
                return constant($constantName);
5618 11239
            }
5619
        }
5620
5621 122
        return $defaultValue;
5622
    }
5623 122
5624
    /**
5625 122
     * Add cell reference if needed while making sure that it is the last argument.
5626 57
     */
5627 57
    private function addCellReference(array $args, bool $passCellReference, array|string $functionCall, ?Cell $cell = null): array
5628 2
    {
5629
        if ($passCellReference) {
5630 2
            if (is_array($functionCall)) {
5631 2
                $className = $functionCall[0];
5632 2
                $methodName = $functionCall[1];
5633
5634 2
                $reflectionMethod = new ReflectionMethod($className, $methodName);
5635
                $argumentCount = count($reflectionMethod->getParameters());
5636
                while (count($args) < $argumentCount - 1) {
5637
                    $args[] = null;
5638
                }
5639
            }
5640
5641 121
            $args[] = $cell;
5642
        }
5643
5644
        return $args;
5645
    }
5646
5647 11257
    private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksheet $cellWorksheet, Stack $stack, bool $ignoreScope = false): mixed
5648
    {
5649 11257
        $definedNameScope = $namedRange->getScope();
5650 205
        if ($definedNameScope !== null && $definedNameScope !== $cellWorksheet && !$ignoreScope) {
5651 205
            // The defined name isn't in our current scope, so #REF
5652 205
            $result = ExcelError::REF();
5653
            $stack->push('Error', $result, $namedRange->getName());
5654 205
5655 205
            return $result;
5656 205
        }
5657 49
5658
        $definedNameValue = $namedRange->getValue();
5659
        $definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range';
5660
        $definedNameWorksheet = $namedRange->getWorksheet();
5661 205
5662
        if ($definedNameValue[0] !== '=') {
5663
            $definedNameValue = '=' . $definedNameValue;
5664 11257
        }
5665
5666
        $this->debugLog->writeDebugLog('Defined Name is a %s with a value of %s', $definedNameType, $definedNameValue);
5667 111
5668
        $originalCoordinate = $cell->getCoordinate();
5669 111
        $recursiveCalculationCell = ($definedNameType !== 'Formula' && $definedNameWorksheet !== null && $definedNameWorksheet !== $cellWorksheet)
5670 111
            ? $definedNameWorksheet->getCell('A1')
5671
            : $cell;
5672
        $recursiveCalculationCellAddress = $recursiveCalculationCell->getCoordinate();
5673
5674
        // Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns
5675
        $definedNameValue = self::$referenceHelper->updateFormulaReferencesAnyWorksheet(
5676
            $definedNameValue,
5677
            Coordinate::columnIndexFromString($cell->getColumn()) - 1,
5678 111
            $cell->getRow() - 1
5679 111
        );
5680 111
5681
        $this->debugLog->writeDebugLog('Value adjusted for relative references is %s', $definedNameValue);
5682 111
5683 89
        $recursiveCalculator = new self($this->spreadsheet);
5684
        $recursiveCalculator->getDebugLog()->setWriteDebugLog($this->getDebugLog()->getWriteDebugLog());
5685
        $recursiveCalculator->getDebugLog()->setEchoDebugLog($this->getDebugLog()->getEchoDebugLog());
5686 111
        $result = $recursiveCalculator->_calculateFormulaValue($definedNameValue, $recursiveCalculationCellAddress, $recursiveCalculationCell, true);
5687
        $cellWorksheet->getCell($originalCoordinate);
5688 111
5689 111
        if ($this->getDebugLog()->getWriteDebugLog()) {
5690 16
            $this->debugLog->mergeDebugLog(array_slice($recursiveCalculator->getDebugLog()->getLog(), 3));
5691 104
            $this->debugLog->writeDebugLog('Evaluation Result for Named %s %s is %s', $definedNameType, $namedRange->getName(), $this->showTypeDetails($result));
5692 111
        }
5693
5694
        $stack->push('Defined Name', $result, $namedRange->getName());
5695 111
5696 111
        return $result;
5697 111
    }
5698 111
5699 111
    public function setSuppressFormulaErrors(bool $suppressFormulaErrors): void
5700
    {
5701 111
        $this->suppressFormulaErrors = $suppressFormulaErrors;
5702
    }
5703 111
5704 111
    public function getSuppressFormulaErrors(): bool
5705 111
    {
5706 111
        return $this->suppressFormulaErrors;
5707 111
    }
5708
5709 111
    public static function boolToString(mixed $operand1): mixed
5710
    {
5711
        if (is_bool($operand1)) {
5712
            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
5713
        } elseif ($operand1 === null) {
5714 111
            $operand1 = '';
5715
        }
5716 111
5717
        return $operand1;
5718
    }
5719 2
5720
    private static function isNumericOrBool(mixed $operand): bool
5721 2
    {
5722
        return is_numeric($operand) || is_bool($operand);
5723
    }
5724 4
5725
    private static function makeError(mixed $operand = ''): string
5726 4
    {
5727
        return Information\ErrorValue::isError($operand) ? $operand : ExcelError::VALUE();
5728
    }
5729
}
5730