Passed
Pull Request — master (#3339)
by Mark
13:02
created

Calculation::disableBranchPruning()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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