Passed
Push — master ( 10b137...2bffcf )
by Adrien
10:25
created

Calculation::setLocale()   D

Complexity

Conditions 18
Paths 44

Size

Total Lines 79
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 18.2433

Importance

Changes 0
Metric Value
cc 18
eloc 46
c 0
b 0
f 0
nc 44
nop 1
dl 0
loc 79
rs 4.8666
ccs 40
cts 44
cp 0.9091
crap 18.2433

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

4521
                            $sheet1 = ($pCellParent !== null) ? $pCellWorksheet->/** @scrutinizer ignore-call */ getTitle() : '';

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
4522
                        }
4523
4524 442
                        [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true);
4525 442
                        if (empty($sheet2)) {
4526 2
                            $sheet2 = $sheet1;
4527
                        }
4528
4529 442
                        if (trim($sheet1, "'") === trim($sheet2, "'")) {
4530 442
                            if ($operand1Data['reference'] === null) {
4531
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
4532
                                    $operand1Data['reference'] = $cell->getColumn() . $operand1Data['value'];
1 ignored issue
show
Bug introduced by
The method getColumn() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

4532
                                    $operand1Data['reference'] = $cell->/** @scrutinizer ignore-call */ getColumn() . $operand1Data['value'];

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
4533
                                } elseif (trim($operand1Data['reference']) == '') {
4534
                                    $operand1Data['reference'] = $cell->getCoordinate();
4535
                                } else {
4536
                                    $operand1Data['reference'] = $operand1Data['value'] . $cell->getRow();
4537
                                }
4538
                            }
4539 442
                            if ($operand2Data['reference'] === null) {
4540
                                if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
4541
                                    $operand2Data['reference'] = $cell->getColumn() . $operand2Data['value'];
4542
                                } elseif (trim($operand2Data['reference']) == '') {
4543
                                    $operand2Data['reference'] = $cell->getCoordinate();
4544
                                } else {
4545
                                    $operand2Data['reference'] = $operand2Data['value'] . $cell->getRow();
4546
                                }
4547
                            }
4548
4549 442
                            $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
4550 442
                            $oCol = $oRow = [];
4551 442
                            foreach ($oData as $oDatum) {
4552 442
                                $oCR = Coordinate::coordinateFromString($oDatum);
4553 442
                                $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
4554 442
                                $oRow[] = $oCR[1];
4555
                            }
4556 442
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4557 442
                            if ($pCellParent !== null) {
4558 442
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
4559
                            } else {
4560
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4561
                            }
4562
4563 442
                            $stack->push('Cell Reference', $cellValue, $cellRef);
4564
                        } else {
4565
                            $stack->push('Error', Functions::REF(), null);
4566
                        }
4567
4568 442
                        break;
4569 190
                    case '+':            //    Addition
4570 77
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
4571 77
                        if (isset($storeKey)) {
4572 1
                            $branchStore[$storeKey] = $result;
4573
                        }
4574
4575 77
                        break;
4576 151
                    case '-':            //    Subtraction
4577 23
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
4578 23
                        if (isset($storeKey)) {
4579
                            $branchStore[$storeKey] = $result;
4580
                        }
4581
4582 23
                        break;
4583 133
                    case '*':            //    Multiplication
4584 79
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
4585 79
                        if (isset($storeKey)) {
4586
                            $branchStore[$storeKey] = $result;
4587
                        }
4588
4589 79
                        break;
4590 63
                    case '/':            //    Division
4591 40
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
4592 40
                        if (isset($storeKey)) {
4593
                            $branchStore[$storeKey] = $result;
4594
                        }
4595
4596 40
                        break;
4597 25
                    case '^':            //    Exponential
4598 1
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
4599 1
                        if (isset($storeKey)) {
4600
                            $branchStore[$storeKey] = $result;
4601
                        }
4602
4603 1
                        break;
4604 24
                    case '&':            //    Concatenation
4605
                        //    If either of the operands is a matrix, we need to treat them both as matrices
4606
                        //        (converting the other operand to a matrix if need be); then perform the required
4607
                        //        matrix operation
4608 10
                        if (is_bool($operand1)) {
4609
                            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
4610
                        }
4611 10
                        if (is_bool($operand2)) {
4612
                            $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
4613
                        }
4614 10
                        if ((is_array($operand1)) || (is_array($operand2))) {
4615
                            //    Ensure that both operands are arrays/matrices
4616 7
                            self::checkMatrixOperands($operand1, $operand2, 2);
4617
4618
                            try {
4619
                                //    Convert operand 1 from a PHP array to a matrix
4620 7
                                $matrix = new Shared\JAMA\Matrix($operand1);
4621
                                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4622 7
                                $matrixResult = $matrix->concat($operand2);
4623 7
                                $result = $matrixResult->getArray();
4624
                            } catch (\Exception $ex) {
4625
                                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4626 7
                                $result = '#VALUE!';
4627
                            }
4628
                        } else {
4629 3
                            $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE;
4630
                        }
4631 10
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
4632 10
                        $stack->push('Value', $result);
4633
4634 10
                        if (isset($storeKey)) {
4635
                            $branchStore[$storeKey] = $result;
4636
                        }
4637
4638 10
                        break;
4639 14
                    case '|':            //    Intersect
4640 14
                        $rowIntersect = array_intersect_key($operand1, $operand2);
4641 14
                        $cellIntersect = $oCol = $oRow = [];
4642 14
                        foreach (array_keys($rowIntersect) as $row) {
4643 14
                            $oRow[] = $row;
4644 14
                            foreach ($rowIntersect[$row] as $col => $data) {
4645 14
                                $oCol[] = Coordinate::columnIndexFromString($col) - 1;
4646 14
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
4647
                            }
4648
                        }
4649 14
                        if (count(Functions::flattenArray($cellIntersect)) === 0) {
4650 2
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
4651 2
                            $stack->push('Error', Functions::null(), null);
4652
                        } else {
4653 12
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' .
4654 12
                                Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4655 12
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
4656 12
                            $stack->push('Value', $cellIntersect, $cellRef);
4657
                        }
4658
4659 713
                        break;
4660
                }
4661
4662
                // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
4663 4022
            } elseif (($token === '~') || ($token === '%')) {
4664 301
                if (($arg = $stack->pop()) === null) {
4665
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4666
                }
4667 301
                $arg = $arg['value'];
4668 301
                if ($token === '~') {
4669 299
                    $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg));
4670 299
                    $multiplier = -1;
4671
                } else {
4672 2
                    $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg));
4673 2
                    $multiplier = 0.01;
4674
                }
4675 301
                if (is_array($arg)) {
4676
                    self::checkMatrixOperands($arg, $multiplier, 2);
4677
4678
                    try {
4679
                        $matrix1 = new Shared\JAMA\Matrix($arg);
4680
                        $matrixResult = $matrix1->arrayTimesEquals($multiplier);
4681
                        $result = $matrixResult->getArray();
4682
                    } catch (\Exception $ex) {
4683
                        $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4684
                        $result = '#VALUE!';
4685
                    }
4686
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
4687
                    $stack->push('Value', $result);
4688
                    if (isset($storeKey)) {
4689
                        $branchStore[$storeKey] = $result;
4690
                    }
4691
                } else {
4692 301
                    $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack);
4693
                }
4694 4022
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token ?? '', $matches)) {
4695 1752
                $cellRef = null;
4696
4697 1752
                if (isset($matches[8])) {
4698
                    if ($cell === null) {
4699
                        //                        We can't access the range, so return a REF error
4700
                        $cellValue = Functions::REF();
4701
                    } else {
4702
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
4703
                        if ($matches[2] > '') {
4704
                            $matches[2] = trim($matches[2], "\"'");
4705
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
4706
                                //    It's a Reference to an external spreadsheet (not currently supported)
4707
                                return $this->raiseFormulaError('Unable to access External Workbook');
4708
                            }
4709
                            $matches[2] = trim($matches[2], "\"'");
4710
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]);
4711
                            if ($pCellParent !== null) {
4712
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4713
                            } else {
4714
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4715
                            }
4716
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
4717
                        } else {
4718
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet');
4719
                            if ($pCellParent !== null) {
4720
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4721
                            } else {
4722
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4723
                            }
4724
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
4725
                        }
4726
                    }
4727
                } else {
4728 1752
                    if ($cell === null) {
4729
                        // We can't access the cell, so return a REF error
4730
                        $cellValue = Functions::REF();
4731
                    } else {
4732 1752
                        $cellRef = $matches[6] . $matches[7];
4733 1752
                        if ($matches[2] > '') {
4734 1747
                            $matches[2] = trim($matches[2], "\"'");
4735 1747
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
4736
                                //    It's a Reference to an external spreadsheet (not currently supported)
4737 1
                                return $this->raiseFormulaError('Unable to access External Workbook');
4738
                            }
4739 1747
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]);
4740 1747
                            if ($pCellParent !== null) {
4741 1747
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
4742 1747
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
4743 1586
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4744 1586
                                    $cell->attach($pCellParent);
4745
                                } else {
4746 252
                                    $cellRef = ($cellSheet !== null) ? "'{$matches[2]}'!{$cellRef}" : $cellRef;
4747 1747
                                    $cellValue = null;
4748
                                }
4749
                            } else {
4750
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4751
                            }
4752 1747
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
4753
                        } else {
4754 6
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet');
4755 6
                            if ($pCellParent->has($cellRef)) {
1 ignored issue
show
Bug introduced by
The method has() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

4755
                            if ($pCellParent->/** @scrutinizer ignore-call */ has($cellRef)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
4756 6
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4757 6
                                $cell->attach($pCellParent);
1 ignored issue
show
Bug introduced by
It seems like $pCellParent can also be of type null; however, parameter $parent of PhpOffice\PhpSpreadsheet\Cell\Cell::attach() does only seem to accept PhpOffice\PhpSpreadsheet\Collection\Cells, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4757
                                $cell->attach(/** @scrutinizer ignore-type */ $pCellParent);
Loading history...
4758
                            } else {
4759 2
                                $cellValue = null;
4760
                            }
4761 6
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
4762
                        }
4763
                    }
4764
                }
4765
4766 1752
                $stack->push('Cell Value', $cellValue, $cellRef);
4767 1752
                if (isset($storeKey)) {
4768 1752
                    $branchStore[$storeKey] = $cellValue;
4769
                }
4770
4771
                // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
4772 4002
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) {
4773 3857
                if ($pCellParent) {
4774 3791
                    $cell->attach($pCellParent);
4775
                }
4776
4777 3857
                $functionName = $matches[1];
4778 3857
                $argCount = $stack->pop();
4779 3857
                $argCount = $argCount['value'];
4780 3857
                if ($functionName !== 'MKMATRIX') {
4781 3857
                    $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's'));
4782
                }
4783 3857
                if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) {    // function
4784 3857
                    $passByReference = false;
4785 3857
                    $passCellReference = false;
4786 3857
                    $functionCall = null;
4787 3857
                    if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
4788 3848
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
4789 3848
                        $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
4790 3848
                        $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
4791 32
                    } elseif (isset(self::$controlFunctions[$functionName])) {
4792 32
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
4793 32
                        $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
4794 32
                        $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
4795
                    }
4796
4797
                    // get the arguments for this function
4798 3857
                    $args = $argArrayVals = [];
4799 3857
                    $emptyArguments = [];
4800 3857
                    for ($i = 0; $i < $argCount; ++$i) {
4801 3847
                        $arg = $stack->pop();
4802 3847
                        $a = $argCount - $i - 1;
4803
                        if (
4804 3847
                            ($passByReference) &&
4805 3847
                            (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) &&
4806 3847
                            (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])
4807
                        ) {
4808 25
                            if ($arg['reference'] === null) {
4809
                                $args[] = $cellID;
4810
                                if ($functionName !== 'MKMATRIX') {
4811
                                    $argArrayVals[] = $this->showValue($cellID);
4812
                                }
4813
                            } else {
4814 25
                                $args[] = $arg['reference'];
4815 25
                                if ($functionName !== 'MKMATRIX') {
4816 25
                                    $argArrayVals[] = $this->showValue($arg['reference']);
4817
                                }
4818
                            }
4819
                        } else {
4820 3827
                            $emptyArguments[] = ($arg['type'] === 'Empty Argument');
4821 3827
                            $args[] = self::unwrapResult($arg['value']);
4822 3827
                            if ($functionName !== 'MKMATRIX') {
4823 3827
                                $argArrayVals[] = $this->showValue($arg['value']);
4824
                            }
4825
                        }
4826
                    }
4827
4828
                    //    Reverse the order of the arguments
4829 3857
                    krsort($args);
4830 3857
                    krsort($emptyArguments);
4831
4832 3857
                    if ($argCount > 0) {
4833 3847
                        $args = $this->addDefaultArgumentValues($functionCall, $args, $emptyArguments);
4834
                    }
4835
4836 3857
                    if (($passByReference) && ($argCount == 0)) {
4837 6
                        $args[] = $cellID;
4838 6
                        $argArrayVals[] = $this->showValue($cellID);
4839
                    }
4840
4841 3857
                    if ($functionName !== 'MKMATRIX') {
4842 3857
                        if ($this->debugLog->getWriteDebugLog()) {
4843
                            krsort($argArrayVals);
4844
                            $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )');
4845
                        }
4846
                    }
4847
4848
                    //    Process the argument with the appropriate function call
4849 3857
                    $args = $this->addCellReference($args, $passCellReference, $functionCall, $cell);
4850
4851 3857
                    if (!is_array($functionCall)) {
4852 51
                        foreach ($args as &$arg) {
4853
                            $arg = Functions::flattenSingleValue($arg);
4854
                        }
4855 51
                        unset($arg);
4856
                    }
4857
4858 3857
                    $result = call_user_func_array($functionCall, $args);
4859
4860 3852
                    if ($functionName !== 'MKMATRIX') {
4861 3852
                        $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
4862
                    }
4863 3852
                    $stack->push('Value', self::wrapResult($result));
4864 3852
                    if (isset($storeKey)) {
4865 3852
                        $branchStore[$storeKey] = $result;
4866
                    }
4867
                }
4868
            } else {
4869
                // if the token is a number, boolean, string or an Excel error, push it onto the stack
4870 4002
                if (isset(self::$excelConstants[strtoupper($token ?? '')])) {
4871
                    $excelConstant = strtoupper($token);
4872
                    $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
4873
                    if (isset($storeKey)) {
4874
                        $branchStore[$storeKey] = self::$excelConstants[$excelConstant];
4875
                    }
4876
                    $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant]));
4877 4002
                } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) {
4878 3964
                    $stack->push($tokenData['type'], $token, $tokenData['reference']);
4879 3964
                    if (isset($storeKey)) {
4880 3964
                        $branchStore[$storeKey] = $token;
4881
                    }
4882
                    // if the token is a named range or formula, evaluate it and push the result onto the stack
4883 103
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token, $matches)) {
4884 103
                    $definedName = $matches[6];
4885 103
                    if ($cell === null || $pCellWorksheet === null) {
4886
                        return $this->raiseFormulaError("undefined name '$token'");
4887
                    }
4888
4889 103
                    $this->debugLog->writeDebugLog('Evaluating Defined Name ', $definedName);
4890 103
                    $namedRange = DefinedName::resolveName($definedName, $pCellWorksheet);
4891 103
                    if ($namedRange === null) {
4892 23
                        return $this->raiseFormulaError("undefined name '$definedName'");
4893
                    }
4894
4895 89
                    $result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack);
4896 89
                    if (isset($storeKey)) {
4897 89
                        $branchStore[$storeKey] = $result;
4898
                    }
4899
                } else {
4900
                    return $this->raiseFormulaError("undefined name '$token'");
4901
                }
4902
            }
4903
        }
4904
        // when we're out of tokens, the stack should have a single element, the final result
4905 4005
        if ($stack->count() != 1) {
4906
            return $this->raiseFormulaError('internal error');
4907
        }
4908 4005
        $output = $stack->pop();
4909 4005
        $output = $output['value'];
4910
4911 4005
        return $output;
4912
    }
4913
4914 448
    private function validateBinaryOperand(&$operand, &$stack)
4915
    {
4916 448
        if (is_array($operand)) {
4917 108
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
4918
                do {
4919 108
                    $operand = array_pop($operand);
4920 108
                } while (is_array($operand));
4921
            }
4922
        }
4923
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
4924 448
        if (is_string($operand)) {
4925
            //    We only need special validations for the operand if it is a string
4926
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
4927 4
            if ($operand > '' && $operand[0] == self::FORMULA_STRING_QUOTE) {
4928 2
                $operand = self::unwrapResult($operand);
4929
            }
4930
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
4931 4
            if (!is_numeric($operand)) {
4932
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
4933 3
                if ($operand > '' && $operand[0] == '#') {
4934 1
                    $stack->push('Value', $operand);
4935 1
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand));
4936
4937 1
                    return false;
4938 2
                } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
4939
                    //    If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
4940 2
                    $stack->push('Error', '#VALUE!');
4941 2
                    $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!'));
4942
4943 2
                    return false;
4944
                }
4945
            }
4946
        }
4947
4948
        //    return a true if the value of the operand is one that we can use in normal binary operations
4949 447
        return true;
4950
    }
4951
4952
    /**
4953
     * @param null|string $cellID
4954
     * @param mixed $operand1
4955
     * @param mixed $operand2
4956
     * @param string $operation
4957
     *
4958
     * @return array
4959
     */
4960 26
    private function executeArrayComparison($cellID, $operand1, $operand2, $operation, Stack &$stack, bool $recursingArrays)
4961
    {
4962 26
        $result = [];
4963 26
        if (!is_array($operand2)) {
4964
            // Operand 1 is an array, Operand 2 is a scalar
4965 24
            foreach ($operand1 as $x => $operandData) {
4966 24
                $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2));
4967 24
                $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack);
4968 24
                $r = $stack->pop();
4969 24
                $result[$x] = $r['value'];
4970
            }
4971 2
        } elseif (!is_array($operand1)) {
4972
            // Operand 1 is a scalar, Operand 2 is an array
4973
            foreach ($operand2 as $x => $operandData) {
4974
                $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData));
4975
                $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack);
4976
                $r = $stack->pop();
4977
                $result[$x] = $r['value'];
4978
            }
4979
        } else {
4980
            // Operand 1 and Operand 2 are both arrays
4981 2
            if (!$recursingArrays) {
4982 2
                self::checkMatrixOperands($operand1, $operand2, 2);
4983
            }
4984 2
            foreach ($operand1 as $x => $operandData) {
4985 2
                $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x]));
4986 2
                $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true);
4987 2
                $r = $stack->pop();
4988 2
                $result[$x] = $r['value'];
4989
            }
4990
        }
4991
        //    Log the result details
4992 26
        $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result));
4993
        //    And push the result onto the stack
4994 26
        $stack->push('Array', $result);
4995
4996 26
        return $result;
4997
    }
4998
4999
    /**
5000
     * @param null|string $cellID
5001
     * @param mixed $operand1
5002
     * @param mixed $operand2
5003
     * @param string $operation
5004
     * @param bool $recursingArrays
5005
     *
5006
     * @return mixed
5007
     */
5008 187
    private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
5009
    {
5010
        //    If we're dealing with matrix operations, we want a matrix result
5011 187
        if ((is_array($operand1)) || (is_array($operand2))) {
5012 26
            return $this->executeArrayComparison($cellID, $operand1, $operand2, $operation, $stack, $recursingArrays);
5013
        }
5014
5015
        //    Simple validate the two operands if they are string values
5016 187
        if (is_string($operand1) && $operand1 > '' && $operand1[0] == self::FORMULA_STRING_QUOTE) {
5017 115
            $operand1 = self::unwrapResult($operand1);
5018
        }
5019 187
        if (is_string($operand2) && $operand2 > '' && $operand2[0] == self::FORMULA_STRING_QUOTE) {
5020 134
            $operand2 = self::unwrapResult($operand2);
5021
        }
5022
5023
        // Use case insensitive comparaison if not OpenOffice mode
5024 187
        if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
5025 187
            if (is_string($operand1)) {
5026 123
                $operand1 = Shared\StringHelper::strToUpper($operand1);
5027
            }
5028 187
            if (is_string($operand2)) {
5029 136
                $operand2 = Shared\StringHelper::strToUpper($operand2);
5030
            }
5031
        }
5032
5033 187
        $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE;
5034
5035
        //    execute the necessary operation
5036
        switch ($operation) {
5037
            //    Greater than
5038 187
            case '>':
5039 48
                if ($useLowercaseFirstComparison) {
5040 9
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0;
5041
                } else {
5042 48
                    $result = ($operand1 > $operand2);
5043
                }
5044
5045 48
                break;
5046
            //    Less than
5047 169
            case '<':
5048 23
                if ($useLowercaseFirstComparison) {
5049 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0;
5050
                } else {
5051 23
                    $result = ($operand1 < $operand2);
5052
                }
5053
5054 23
                break;
5055
            //    Equality
5056 156
            case '=':
5057 109
                if (is_numeric($operand1) && is_numeric($operand2)) {
5058 25
                    $result = (abs($operand1 - $operand2) < $this->delta);
5059
                } else {
5060 97
                    $result = $this->strcmpAllowNull($operand1, $operand2) == 0;
5061
                }
5062
5063 109
                break;
5064
            //    Greater than or equal
5065 48
            case '>=':
5066 9
                if (is_numeric($operand1) && is_numeric($operand2)) {
5067 5
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2));
5068 4
                } elseif ($useLowercaseFirstComparison) {
5069 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0;
5070
                } else {
5071 4
                    $result = $this->strcmpAllowNull($operand1, $operand2) >= 0;
5072
                }
5073
5074 9
                break;
5075
            //    Less than or equal
5076 39
            case '<=':
5077 10
                if (is_numeric($operand1) && is_numeric($operand2)) {
5078 6
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2));
5079 6
                } elseif ($useLowercaseFirstComparison) {
5080 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0;
5081
                } else {
5082 6
                    $result = $this->strcmpAllowNull($operand1, $operand2) <= 0;
5083
                }
5084
5085 10
                break;
5086
            //    Inequality
5087 29
            case '<>':
5088 29
                if (is_numeric($operand1) && is_numeric($operand2)) {
5089 3
                    $result = (abs($operand1 - $operand2) > 1E-14);
5090
                } else {
5091 26
                    $result = $this->strcmpAllowNull($operand1, $operand2) != 0;
5092
                }
5093
5094 29
                break;
5095
5096
            default:
5097
                throw new Exception('Unsupported binary comparison operation');
5098
        }
5099
5100
        //    Log the result details
5101 187
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
5102
        //    And push the result onto the stack
5103 187
        $stack->push('Value', $result);
5104
5105 187
        return $result;
5106
    }
5107
5108
    /**
5109
     * Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
5110
     *
5111
     * @param null|string $str1 First string value for the comparison
5112
     * @param null|string $str2 Second string value for the comparison
5113
     *
5114
     * @return int
5115
     */
5116 21
    private function strcmpLowercaseFirst($str1, $str2)
5117
    {
5118 21
        $inversedStr1 = Shared\StringHelper::strCaseReverse($str1);
5119 21
        $inversedStr2 = Shared\StringHelper::strCaseReverse($str2);
5120
5121 21
        return strcmp($inversedStr1 ?? '', $inversedStr2 ?? '');
5122
    }
5123
5124
    /**
5125
     * PHP8.1 deprecates passing null to strcmp.
5126
     *
5127
     * @param null|string $str1 First string value for the comparison
5128
     * @param null|string $str2 Second string value for the comparison
5129
     *
5130
     * @return int
5131
     */
5132 132
    private function strcmpAllowNull($str1, $str2)
5133
    {
5134 132
        return strcmp($str1 ?? '', $str2 ?? '');
5135
    }
5136
5137
    /**
5138
     * @param mixed $operand1
5139
     * @param mixed $operand2
5140
     * @param mixed $operation
5141
     * @param string $matrixFunction
5142
     * @param mixed $stack
5143
     *
5144
     * @return bool|mixed
5145
     */
5146 448
    private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack)
5147
    {
5148
        //    Validate the two operands
5149 448
        if (!$this->validateBinaryOperand($operand1, $stack)) {
5150 1
            return false;
5151
        }
5152 447
        if (!$this->validateBinaryOperand($operand2, $stack)) {
5153 2
            return false;
5154
        }
5155
5156
        //    If either of the operands is a matrix, we need to treat them both as matrices
5157
        //        (converting the other operand to a matrix if need be); then perform the required
5158
        //        matrix operation
5159 445
        if ((is_array($operand1)) || (is_array($operand2))) {
5160
            //    Ensure that both operands are arrays/matrices of the same size
5161
            self::checkMatrixOperands($operand1, $operand2, 2);
5162
5163
            try {
5164
                //    Convert operand 1 from a PHP array to a matrix
5165
                $matrix = new Shared\JAMA\Matrix($operand1);
5166
                //    Perform the required operation against the operand 1 matrix, passing in operand 2
5167
                $matrixResult = $matrix->$matrixFunction($operand2);
5168
                $result = $matrixResult->getArray();
5169
            } catch (\Exception $ex) {
5170
                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
5171
                $result = '#VALUE!';
5172
            }
5173
        } else {
5174
            if (
5175 445
                (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) &&
5176 413
                ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) ||
5177 445
                    (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0))
5178
            ) {
5179
                $result = Functions::VALUE();
5180
            } else {
5181
                //    If we're dealing with non-matrix operations, execute the necessary operation
5182
                switch ($operation) {
5183
                    //    Addition
5184 445
                    case '+':
5185 74
                        $result = $operand1 + $operand2;
5186
5187 74
                        break;
5188
                    //    Subtraction
5189 409
                    case '-':
5190 23
                        $result = $operand1 - $operand2;
5191
5192 23
                        break;
5193
                    //    Multiplication
5194 394
                    case '*':
5195 370
                        $result = $operand1 * $operand2;
5196
5197 370
                        break;
5198
                    //    Division
5199 41
                    case '/':
5200 40
                        if ($operand2 == 0) {
5201
                            //    Trap for Divide by Zero error
5202 10
                            $stack->push('Error', '#DIV/0!');
5203 10
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails('#DIV/0!'));
5204
5205 10
                            return false;
5206
                        }
5207 30
                        $result = $operand1 / $operand2;
5208
5209 30
                        break;
5210
                    //    Power
5211 1
                    case '^':
5212 1
                        $result = $operand1 ** $operand2;
5213
5214 1
                        break;
5215
5216
                    default:
5217
                        throw new Exception('Unsupported numeric binary operation');
5218
                }
5219
            }
5220
        }
5221
5222
        //    Log the result details
5223 437
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
5224
        //    And push the result onto the stack
5225 437
        $stack->push('Value', $result);
5226
5227 437
        return $result;
5228
    }
5229
5230
    // trigger an error, but nicely, if need be
5231 174
    protected function raiseFormulaError($errorMessage)
5232
    {
5233 174
        $this->formulaError = $errorMessage;
5234 174
        $this->cyclicReferenceStack->clear();
5235 174
        if (!$this->suppressFormulaErrors) {
5236 174
            throw new Exception($errorMessage);
5237
        }
5238
        trigger_error($errorMessage, E_USER_ERROR);
5239
5240
        return false;
5241
    }
5242
5243
    /**
5244
     * Extract range values.
5245
     *
5246
     * @param string $range String based range representation
5247
     * @param Worksheet $worksheet Worksheet
5248
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
5249
     *
5250
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
5251
     */
5252 1630
    public function extractCellRange(&$range = 'A1', ?Worksheet $worksheet = null, $resetLog = true)
5253
    {
5254
        // Return value
5255 1630
        $returnValue = [];
5256
5257 1630
        if ($worksheet !== null) {
5258 1630
            $worksheetName = $worksheet->getTitle();
5259
5260 1630
            if (strpos($range, '!') !== false) {
5261 10
                [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true);
5262 10
                $worksheet = $this->spreadsheet->getSheetByName($worksheetName);
5263
            }
5264
5265
            // Extract range
5266 1630
            $aReferences = Coordinate::extractAllCellReferencesInRange($range);
5267 1630
            $range = "'" . $worksheetName . "'" . '!' . $range;
5268 1630
            if (!isset($aReferences[1])) {
5269 1600
                $currentCol = '';
5270 1600
                $currentRow = 0;
5271
                //    Single cell in range
5272 1600
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
5273 1600
                if ($worksheet->cellExists($aReferences[0])) {
5274 1599
                    $returnValue[$currentRow][$currentCol] = $worksheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5275
                } else {
5276 1600
                    $returnValue[$currentRow][$currentCol] = null;
5277
                }
5278
            } else {
5279
                // Extract cell data for all cells in the range
5280 454
                foreach ($aReferences as $reference) {
5281 454
                    $currentCol = '';
5282 454
                    $currentRow = 0;
5283
                    // Extract range
5284 454
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
5285 454
                    if ($worksheet->cellExists($reference)) {
5286 425
                        $returnValue[$currentRow][$currentCol] = $worksheet->getCell($reference)->getCalculatedValue($resetLog);
5287
                    } else {
5288 72
                        $returnValue[$currentRow][$currentCol] = null;
5289
                    }
5290
                }
5291
            }
5292
        }
5293
5294 1630
        return $returnValue;
5295
    }
5296
5297
    /**
5298
     * Extract range values.
5299
     *
5300
     * @param string $range String based range representation
5301
     * @param null|Worksheet $worksheet Worksheet
5302
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
5303
     *
5304
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
5305
     */
5306
    public function extractNamedRange(string &$range = 'A1', ?Worksheet $worksheet = null, $resetLog = true)
5307
    {
5308
        // Return value
5309
        $returnValue = [];
5310
5311
        if ($worksheet !== null) {
5312
            if (strpos($range, '!') !== false) {
5313
                [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true);
5314
                $worksheet = $this->spreadsheet->getSheetByName($worksheetName);
5315
            }
5316
5317
            // Named range?
5318
            $namedRange = DefinedName::resolveName($range, $worksheet);
1 ignored issue
show
Bug introduced by
It seems like $worksheet can also be of type null; however, parameter $worksheet of PhpOffice\PhpSpreadsheet...inedName::resolveName() does only seem to accept PhpOffice\PhpSpreadsheet\Worksheet\Worksheet, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5318
            $namedRange = DefinedName::resolveName($range, /** @scrutinizer ignore-type */ $worksheet);
Loading history...
5319
            if ($namedRange === null) {
5320
                return Functions::REF();
5321
            }
5322
5323
            $worksheet = $namedRange->getWorksheet();
5324
            $range = $namedRange->getValue();
5325
            $splitRange = Coordinate::splitRange($range);
5326
            //    Convert row and column references
5327
            if (ctype_alpha($splitRange[0][0])) {
5328
                $range = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
5329
            } elseif (ctype_digit($splitRange[0][0])) {
5330
                $range = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
5331
            }
5332
5333
            // Extract range
5334
            $aReferences = Coordinate::extractAllCellReferencesInRange($range);
5335
            if (!isset($aReferences[1])) {
5336
                //    Single cell (or single column or row) in range
5337
                [$currentCol, $currentRow] = Coordinate::coordinateFromString($aReferences[0]);
5338
                if ($worksheet->cellExists($aReferences[0])) {
5339
                    $returnValue[$currentRow][$currentCol] = $worksheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5340
                } else {
5341
                    $returnValue[$currentRow][$currentCol] = null;
5342
                }
5343
            } else {
5344
                // Extract cell data for all cells in the range
5345
                foreach ($aReferences as $reference) {
5346
                    // Extract range
5347
                    [$currentCol, $currentRow] = Coordinate::coordinateFromString($reference);
5348
                    if ($worksheet->cellExists($reference)) {
5349
                        $returnValue[$currentRow][$currentCol] = $worksheet->getCell($reference)->getCalculatedValue($resetLog);
5350
                    } else {
5351
                        $returnValue[$currentRow][$currentCol] = null;
5352
                    }
5353
                }
5354
            }
5355
        }
5356
5357
        return $returnValue;
5358
    }
5359
5360
    /**
5361
     * Is a specific function implemented?
5362
     *
5363
     * @param string $function Function Name
5364
     *
5365
     * @return bool
5366
     */
5367 3
    public function isImplemented($function)
5368
    {
5369 3
        $function = strtoupper($function);
5370 3
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$function]) || (is_array(self::$phpSpreadsheetFunctions[$function]['functionCall']) && self::$phpSpreadsheetFunctions[$function]['functionCall'][1] === 'DUMMY');
5371
5372 3
        return !$notImplemented;
5373
    }
5374
5375
    /**
5376
     * Get a list of all implemented functions as an array of function objects.
5377
     */
5378
    public function getFunctions(): array
5379
    {
5380
        return self::$phpSpreadsheetFunctions;
5381
    }
5382
5383
    /**
5384
     * Get a list of implemented Excel function names.
5385
     *
5386
     * @return array
5387
     */
5388 2
    public function getImplementedFunctionNames()
5389
    {
5390 2
        $returnValue = [];
5391 2
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
5392 2
            if ($this->isImplemented($functionName)) {
5393 2
                $returnValue[] = $functionName;
5394
            }
5395
        }
5396
5397 2
        return $returnValue;
5398
    }
5399
5400 3847
    private function addDefaultArgumentValues(array $functionCall, array $args, array $emptyArguments): array
5401
    {
5402 3847
        $reflector = new ReflectionMethod(implode('::', $functionCall));
5403 3847
        $methodArguments = $reflector->getParameters();
5404
5405 3847
        if (count($methodArguments) > 0) {
5406
            // Apply any defaults for empty argument values
5407 3847
            foreach ($emptyArguments as $argumentId => $isArgumentEmpty) {
5408 3827
                if ($isArgumentEmpty === true) {
5409 28
                    $reflectedArgumentId = count($args) - (int) $argumentId - 1;
5410
                    if (
5411 28
                        !array_key_exists($reflectedArgumentId, $methodArguments) ||
5412 28
                        $methodArguments[$reflectedArgumentId]->isVariadic()
5413
                    ) {
5414 1
                        break;
5415
                    }
5416
5417 27
                    $args[$argumentId] = $this->getArgumentDefaultValue($methodArguments[$reflectedArgumentId]);
5418
                }
5419
            }
5420
        }
5421
5422 3847
        return $args;
5423
    }
5424
5425
    /**
5426
     * @return null|mixed
5427
     */
5428 27
    private function getArgumentDefaultValue(ReflectionParameter $methodArgument)
5429
    {
5430 27
        $defaultValue = null;
5431
5432 27
        if ($methodArgument->isDefaultValueAvailable()) {
5433 5
            $defaultValue = $methodArgument->getDefaultValue();
5434 5
            if ($methodArgument->isDefaultValueConstant()) {
5435 3
                $constantName = $methodArgument->getDefaultValueConstantName() ?? '';
5436
                // read constant value
5437 3
                if (strpos($constantName, '::') !== false) {
5438 3
                    [$className, $constantName] = explode('::', $constantName);
5439 3
                    $constantReflector = new ReflectionClassConstant($className, $constantName);
5440
5441 3
                    return $constantReflector->getValue();
5442
                }
5443
5444
                return constant($constantName);
5445
            }
5446
        }
5447
5448 26
        return $defaultValue;
5449
    }
5450
5451
    /**
5452
     * Add cell reference if needed while making sure that it is the last argument.
5453
     *
5454
     * @param bool $passCellReference
5455
     * @param array|string $functionCall
5456
     *
5457
     * @return array
5458
     */
5459 3857
    private function addCellReference(array $args, $passCellReference, $functionCall, ?Cell $cell = null)
5460
    {
5461 3857
        if ($passCellReference) {
5462 109
            if (is_array($functionCall)) {
5463 109
                $className = $functionCall[0];
5464 109
                $methodName = $functionCall[1];
5465
5466 109
                $reflectionMethod = new ReflectionMethod($className, $methodName);
5467 109
                $argumentCount = count($reflectionMethod->getParameters());
5468 109
                while (count($args) < $argumentCount - 1) {
5469 32
                    $args[] = null;
5470
                }
5471
            }
5472
5473 109
            $args[] = $cell;
5474
        }
5475
5476 3857
        return $args;
5477
    }
5478
5479 34
    private function getUnusedBranchStoreKey()
5480
    {
5481 34
        $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter;
5482 34
        ++$this->branchStoreKeyCounter;
5483
5484 34
        return $storeKeyValue;
5485
    }
5486
5487
    private function getTokensAsString($tokens)
5488
    {
5489
        $tokensStr = array_map(function ($token) {
5490
            $value = $token['value'] ?? 'no value';
5491
            while (is_array($value)) {
5492
                $value = array_pop($value);
5493
            }
5494
5495
            return $value;
5496
        }, $tokens);
5497
5498
        return '[ ' . implode(' | ', $tokensStr) . ' ]';
5499
    }
5500
5501
    /**
5502
     * @return mixed|string
5503
     */
5504 89
    private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksheet $cellWorksheet, Stack $stack)
5505
    {
5506 89
        $definedNameScope = $namedRange->getScope();
5507 89
        if ($definedNameScope !== null && $definedNameScope !== $cellWorksheet) {
5508
            // The defined name isn't in our current scope, so #REF
5509
            $result = Functions::REF();
5510
            $stack->push('Error', $result, $namedRange->getName());
5511
5512
            return $result;
5513
        }
5514
5515 89
        $definedNameValue = $namedRange->getValue();
5516 89
        $definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range';
5517 89
        $definedNameWorksheet = $namedRange->getWorksheet();
5518
5519 89
        if ($definedNameValue[0] !== '=') {
5520 78
            $definedNameValue = '=' . $definedNameValue;
5521
        }
5522
5523 89
        $this->debugLog->writeDebugLog("Defined Name is a {$definedNameType} with a value of {$definedNameValue}");
5524
5525 89
        $recursiveCalculationCell = ($definedNameWorksheet !== null && $definedNameWorksheet !== $cellWorksheet)
5526 8
            ? $definedNameWorksheet->getCell('A1')
5527 89
            : $cell;
5528 89
        $recursiveCalculationCellAddress = $recursiveCalculationCell->getCoordinate();
5529
5530
        // Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns
5531 89
        $definedNameValue = self::$referenceHelper->updateFormulaReferencesAnyWorksheet(
5532
            $definedNameValue,
5533 89
            Coordinate::columnIndexFromString($cell->getColumn()) - 1,
5534 89
            $cell->getRow() - 1
5535
        );
5536
5537 89
        $this->debugLog->writeDebugLog("Value adjusted for relative references is {$definedNameValue}");
5538
5539 89
        $recursiveCalculator = new self($this->spreadsheet);
5540 89
        $recursiveCalculator->getDebugLog()->setWriteDebugLog($this->getDebugLog()->getWriteDebugLog());
5541 89
        $recursiveCalculator->getDebugLog()->setEchoDebugLog($this->getDebugLog()->getEchoDebugLog());
5542 89
        $result = $recursiveCalculator->_calculateFormulaValue($definedNameValue, $recursiveCalculationCellAddress, $recursiveCalculationCell);
5543
5544 89
        if ($this->getDebugLog()->getWriteDebugLog()) {
5545
            $this->debugLog->mergeDebugLog(array_slice($recursiveCalculator->getDebugLog()->getLog(), 3));
5546
            $this->debugLog->writeDebugLog("Evaluation Result for Named {$definedNameType} {$namedRange->getName()} is {$this->showTypeDetails($result)}");
5547
        }
5548
5549 89
        $stack->push('Defined Name', $result, $namedRange->getName());
5550
5551 89
        return $result;
5552
    }
5553
}
5554