Failed Conditions
Pull Request — master (#1694)
by Adrien
08:05
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 4784
    public function __construct(?Spreadsheet $spreadsheet = null)
2761
    {
2762 4784
        $this->delta = 1 * 10 ** (0 - ini_get('precision'));
2763
2764 4784
        $this->spreadsheet = $spreadsheet;
2765 4784
        $this->cyclicReferenceStack = new CyclicReferenceStack();
2766 4784
        $this->debugLog = new Logger($this->cyclicReferenceStack);
2767 4784
        self::$referenceHelper = ReferenceHelper::getInstance();
2768 4784
    }
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 5012
    public static function getInstance(?Spreadsheet $spreadsheet = null): self
2788
    {
2789 5012
        if ($spreadsheet !== null) {
2790 4331
            $instance = $spreadsheet->getCalculationEngine();
2791 4331
            if (isset($instance)) {
2792 4331
                return $instance;
2793
            }
2794
        }
2795
2796 1178
        if (!isset(self::$instance) || (self::$instance === null)) {
2797 13
            self::$instance = new self();
2798
        }
2799
2800 1178
        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 456
    public function getDebugLog()
2819
    {
2820 456
        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 $pValue
2897
     */
2898
    public function setCalculationCacheEnabled($pValue): void
2899
    {
2900
        $this->calculationCacheEnabled = $pValue;
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 33
    public function clearCalculationCacheForWorksheet($worksheetName): void
2934
    {
2935 33
        if (isset($this->calculationCache[$worksheetName])) {
2936
            unset($this->calculationCache[$worksheetName]);
2937
        }
2938 33
    }
2939
2940
    /**
2941
     * Rename calculation cache for a specified worksheet.
2942
     *
2943
     * @param string $fromWorksheetName
2944
     * @param string $toWorksheetName
2945
     */
2946 4784
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName): void
2947
    {
2948 4784
        if (isset($this->calculationCache[$fromWorksheetName])) {
2949
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2950
            unset($this->calculationCache[$fromWorksheetName]);
2951
        }
2952 4784
    }
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 $pCell Cell to calculate
3290
     *
3291
     * @return mixed
3292
     */
3293
    public function calculate(?Cell $pCell = null)
3294
    {
3295
        try {
3296
            return $this->calculateCellValue($pCell);
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 $pCell 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 $pCell = null, $resetLog = true)
3311
    {
3312 4012
        if ($pCell === 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' => $pCell->getWorksheet()->getTitle(),
3330 4012
            'cell' => $pCell->getCoordinate(),
3331
        ];
3332
3333
        try {
3334 4012
            $result = self::unwrapResult($this->_calculateFormulaValue($pCell->getValue(), $pCell->getCoordinate(), $pCell));
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 && $pCell->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 $pCell Cell to calculate
3409
     *
3410
     * @return mixed
3411
     */
3412 4
    public function calculateFormula($formula, $cellID = null, ?Cell $pCell = 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 && $pCell === null) {
3421
            $cellID = 'A1';
3422
            $pCell = $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, $pCell));
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 $pCell Cell to calculate
3481
     *
3482
     * @return mixed
3483
     */
3484 4179
    public function _calculateFormulaValue($formula, $cellID = null, ?Cell $pCell = null)
3485
    {
3486 4179
        $cellValue = null;
3487
3488
        //  Quote-Prefixed cell values cannot be formulae, but are treated as strings
3489 4179
        if ($pCell !== null && $pCell->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 = ($pCell !== null) ? $pCell->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, $pCell), $cellID, $pCell);
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 $pCell = 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 = ($pCell !== null) ? $pCell->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 127
                    ((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
4334 127
                        ($output[count($output) - 1]['type'] == 'Cell Reference') ||
4335 117
                        (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', substr($formula, $index), $match)) &&
4336 1785
                            ($output[count($output) - 1]['type'] == 'Defined Name' || $output[count($output) - 1]['type'] == 'Value')
4337
                    )
4338
                ) {
4339
                    while (
4340 14
                        $stack->count() > 0 &&
4341 14
                        ($o2 = $stack->last()) &&
4342 14
                        isset(self::$operators[$o2['value']]) &&
4343 14
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])
4344
                    ) {
4345 10
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
4346
                    }
4347 14
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
4348 14
                    $expectingOperator = false;
4349
                }
4350
            }
4351
        }
4352
4353 4028
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
4354 268
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
4355
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
4356
            }
4357 268
            $output[] = $op;
4358
        }
4359
4360 4028
        return $output;
4361
    }
4362
4363 713
    private static function dataTestReference(&$operandData)
4364
    {
4365 713
        $operand = $operandData['value'];
4366 713
        if (($operandData['reference'] === null) && (is_array($operand))) {
4367 2
            $rKeys = array_keys($operand);
4368 2
            $rowKey = array_shift($rKeys);
4369 2
            $cKeys = array_keys(array_keys($operand[$rowKey]));
4370 2
            $colKey = array_shift($cKeys);
4371 2
            if (ctype_upper("$colKey")) {
4372
                $operandData['reference'] = $colKey . $rowKey;
4373
            }
4374
        }
4375
4376 713
        return $operand;
4377
    }
4378
4379
    // evaluate postfix notation
4380
4381
    /**
4382
     * @param mixed $tokens
4383
     * @param null|string $cellID
4384
     *
4385
     * @return array<int, mixed>|false
4386
     */
4387 4022
    private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null)
4388
    {
4389 4022
        if ($tokens == false) {
4390
            return false;
4391
        }
4392
4393
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
4394
        //        so we store the parent cell collection so that we can re-attach it when necessary
4395 4022
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
4396 4022
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
4397 4022
        $stack = new Stack();
4398
4399
        // Stores branches that have been pruned
4400 4022
        $fakedForBranchPruning = [];
4401
        // help us to know when pruning ['branchTestId' => true/false]
4402 4022
        $branchStore = [];
4403
        //    Loop through each token in turn
4404 4022
        foreach ($tokens as $tokenData) {
4405 4022
            $token = $tokenData['value'];
4406
4407
            // Branch pruning: skip useless resolutions
4408 4022
            $storeKey = $tokenData['storeKey'] ?? null;
4409 4022
            if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) {
4410 29
                $onlyIfStoreKey = $tokenData['onlyIf'];
4411 29
                $storeValue = $branchStore[$onlyIfStoreKey] ?? null;
4412 29
                $storeValueAsBool = ($storeValue === null) ?
4413 29
                    true : (bool) Functions::flattenSingleValue($storeValue);
4414 29
                if (is_array($storeValue)) {
4415 24
                    $wrappedItem = end($storeValue);
4416 24
                    $storeValue = end($wrappedItem);
4417
                }
4418
4419
                if (
4420 29
                    isset($storeValue)
4421
                    && (
4422 28
                        !$storeValueAsBool
4423 28
                        || Functions::isError($storeValue)
4424 29
                        || ($storeValue === 'Pruned branch')
4425
                    )
4426
                ) {
4427
                    // If branching value is not true, we don't need to compute
4428 24
                    if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) {
4429 23
                        $stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token);
4430 23
                        $fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true;
4431
                    }
4432
4433 24
                    if (isset($storeKey)) {
4434
                        // We are processing an if condition
4435
                        // We cascade the pruning to the depending branches
4436 1
                        $branchStore[$storeKey] = 'Pruned branch';
4437 1
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4438 1
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4439
                    }
4440
4441 24
                    continue;
4442
                }
4443
            }
4444
4445 4022
            if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) {
4446 28
                $onlyIfNotStoreKey = $tokenData['onlyIfNot'];
4447 28
                $storeValue = $branchStore[$onlyIfNotStoreKey] ?? null;
4448 28
                $storeValueAsBool = ($storeValue === null) ?
4449 28
                    true : (bool) Functions::flattenSingleValue($storeValue);
4450 28
                if (is_array($storeValue)) {
4451 23
                    $wrappedItem = end($storeValue);
4452 23
                    $storeValue = end($wrappedItem);
4453
                }
4454
                if (
4455 28
                    isset($storeValue)
4456
                    && (
4457 27
                        $storeValueAsBool
4458 27
                        || Functions::isError($storeValue)
4459 28
                        || ($storeValue === 'Pruned branch'))
4460
                ) {
4461
                    // If branching value is true, we don't need to compute
4462 26
                    if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) {
4463 26
                        $stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token);
4464 26
                        $fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true;
4465
                    }
4466
4467 26
                    if (isset($storeKey)) {
4468
                        // We are processing an if condition
4469
                        // We cascade the pruning to the depending branches
4470 2
                        $branchStore[$storeKey] = 'Pruned branch';
4471 2
                        $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4472 2
                        $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4473
                    }
4474
4475 26
                    continue;
4476
                }
4477
            }
4478
4479
            // 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
4480 4022
            if (!is_numeric($token) && isset(self::$binaryOperators[$token])) {
4481
                //    We must have two operands, error if we don't
4482 713
                if (($operand2Data = $stack->pop()) === null) {
4483
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4484
                }
4485 713
                if (($operand1Data = $stack->pop()) === null) {
4486
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4487
                }
4488
4489 713
                $operand1 = self::dataTestReference($operand1Data);
4490 713
                $operand2 = self::dataTestReference($operand2Data);
4491
4492
                //    Log what we're doing
4493 713
                if ($token == ':') {
4494 442
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
4495
                } else {
4496 356
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
4497
                }
4498
4499
                //    Process the operation in the appropriate manner
4500
                switch ($token) {
4501
                    //    Comparison (Boolean) Operators
4502 713
                    case '>':            //    Greater than
4503 698
                    case '<':            //    Less than
4504 685
                    case '>=':            //    Greater than or Equal to
4505 677
                    case '<=':            //    Less than or Equal to
4506 667
                    case '=':            //    Equality
4507 585
                    case '<>':            //    Inequality
4508 187
                        $result = $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
4509 187
                        if (isset($storeKey)) {
4510 25
                            $branchStore[$storeKey] = $result;
4511
                        }
4512
4513 187
                        break;
4514
                    //    Binary Operators
4515 573
                    case ':':            //    Range
4516 442
                        if (strpos($operand1Data['reference'], '!') !== false) {
4517 438
                            [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
4518
                        } else {
4519 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

4519
                            $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...
4520
                        }
4521
4522 442
                        [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true);
4523 442
                        if (empty($sheet2)) {
4524 2
                            $sheet2 = $sheet1;
4525
                        }
4526
4527 442
                        if (trim($sheet1, "'") === trim($sheet2, "'")) {
4528 442
                            if ($operand1Data['reference'] === null) {
4529
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
4530
                                    $operand1Data['reference'] = $pCell->getColumn() . $operand1Data['value'];
1 ignored issue
show
Bug introduced by
The method getColumn() does not exist on null. ( Ignorable by Annotation )

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

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

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

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

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

4753
                            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...
4754 6
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4755 6
                                $pCell->attach($pCellParent);
1 ignored issue
show
Bug introduced by
It seems like $pCellParent can also be of type null; however, parameter $parent of PhpOffice\PhpSpreadsheet\Cell\Cell::attach() does only seem to accept PhpOffice\PhpSpreadsheet\Collection\Cells, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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