Completed
Push — master ( 678886...c4895b )
by Adrien
35:35 queued 29:39
created

Calculation::setLocale()   D

Complexity

Conditions 19
Paths 140

Size

Total Lines 78
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 19.4348

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 47
c 1
b 0
f 0
nc 140
nop 1
dl 0
loc 78
ccs 42
cts 47
cp 0.8936
crap 19.4348
rs 4.1833

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

3942
                            $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...
3943
                        }
3944
3945 58
                        [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true);
3946 58
                        if (empty($sheet2)) {
3947 12
                            $sheet2 = $sheet1;
3948
                        }
3949
3950 58
                        if ($sheet1 == $sheet2) {
3951 58
                            if ($operand1Data['reference'] === null) {
3952
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
3953
                                    $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

3953
                                    $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...
3954
                                } elseif (trim($operand1Data['reference']) == '') {
3955
                                    $operand1Data['reference'] = $pCell->getCoordinate();
3956
                                } else {
3957
                                    $operand1Data['reference'] = $operand1Data['value'] . $pCell->getRow();
3958
                                }
3959
                            }
3960 58
                            if ($operand2Data['reference'] === null) {
3961
                                if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
3962
                                    $operand2Data['reference'] = $pCell->getColumn() . $operand2Data['value'];
3963
                                } elseif (trim($operand2Data['reference']) == '') {
3964
                                    $operand2Data['reference'] = $pCell->getCoordinate();
3965
                                } else {
3966
                                    $operand2Data['reference'] = $operand2Data['value'] . $pCell->getRow();
3967
                                }
3968
                            }
3969
3970 58
                            $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
3971 58
                            $oCol = $oRow = [];
3972 58
                            foreach ($oData as $oDatum) {
3973 58
                                $oCR = Coordinate::coordinateFromString($oDatum);
3974 58
                                $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
3975 58
                                $oRow[] = $oCR[1];
3976
                            }
3977 58
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
3978 58
                            if ($pCellParent !== null) {
3979 58
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
3980
                            } else {
3981
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3982
                            }
3983 58
                            $stack->push('Cell Reference', $cellValue, $cellRef);
3984
                        } else {
3985
                            $stack->push('Error', Functions::REF(), null);
3986
                        }
3987
3988 58
                        break;
3989 39
                    case '+':            //    Addition
3990 27
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
3991 27
                        if (isset($storeKey)) {
3992 1
                            $branchStore[$storeKey] = $result;
3993
                        }
3994
3995 27
                        break;
3996 30
                    case '-':            //    Subtraction
3997 7
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
3998 7
                        if (isset($storeKey)) {
3999
                            $branchStore[$storeKey] = $result;
4000
                        }
4001
4002 7
                        break;
4003 28
                    case '*':            //    Multiplication
4004 20
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
4005 20
                        if (isset($storeKey)) {
4006
                            $branchStore[$storeKey] = $result;
4007
                        }
4008
4009 20
                        break;
4010 12
                    case '/':            //    Division
4011 8
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
4012 8
                        if (isset($storeKey)) {
4013
                            $branchStore[$storeKey] = $result;
4014
                        }
4015
4016 8
                        break;
4017 7
                    case '^':            //    Exponential
4018
                        $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
4019
                        if (isset($storeKey)) {
4020
                            $branchStore[$storeKey] = $result;
4021
                        }
4022
4023
                        break;
4024 7
                    case '&':            //    Concatenation
4025
                        //    If either of the operands is a matrix, we need to treat them both as matrices
4026
                        //        (converting the other operand to a matrix if need be); then perform the required
4027
                        //        matrix operation
4028 7
                        if (is_bool($operand1)) {
4029
                            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
4030
                        }
4031 7
                        if (is_bool($operand2)) {
4032
                            $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
4033
                        }
4034 7
                        if ((is_array($operand1)) || (is_array($operand2))) {
4035
                            //    Ensure that both operands are arrays/matrices
4036 5
                            self::checkMatrixOperands($operand1, $operand2, 2);
4037
4038
                            try {
4039
                                //    Convert operand 1 from a PHP array to a matrix
4040 5
                                $matrix = new Shared\JAMA\Matrix($operand1);
4041
                                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4042 5
                                $matrixResult = $matrix->concat($operand2);
4043 5
                                $result = $matrixResult->getArray();
4044
                            } catch (\Exception $ex) {
4045
                                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4046 5
                                $result = '#VALUE!';
4047
                            }
4048
                        } else {
4049 2
                            $result = '"' . str_replace('""', '"', self::unwrapResult($operand1) . self::unwrapResult($operand2)) . '"';
4050
                        }
4051 7
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
4052 7
                        $stack->push('Value', $result);
4053
4054 7
                        if (isset($storeKey)) {
4055
                            $branchStore[$storeKey] = $result;
4056
                        }
4057
4058 7
                        break;
4059
                    case '|':            //    Intersect
4060
                        $rowIntersect = array_intersect_key($operand1, $operand2);
4061
                        $cellIntersect = $oCol = $oRow = [];
4062
                        foreach (array_keys($rowIntersect) as $row) {
4063
                            $oRow[] = $row;
4064
                            foreach ($rowIntersect[$row] as $col => $data) {
4065
                                $oCol[] = Coordinate::columnIndexFromString($col) - 1;
4066
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
4067
                            }
4068
                        }
4069
                        $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4070
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
4071
                        $stack->push('Value', $cellIntersect, $cellRef);
4072
4073 158
                        break;
4074
                }
4075
4076
                // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
4077 185
            } elseif (($token === '~') || ($token === '%')) {
4078 4
                if (($arg = $stack->pop()) === null) {
4079
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4080
                }
4081 4
                $arg = $arg['value'];
4082 4
                if ($token === '~') {
4083 4
                    $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg));
4084 4
                    $multiplier = -1;
4085
                } else {
4086
                    $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg));
4087
                    $multiplier = 0.01;
4088
                }
4089 4
                if (is_array($arg)) {
4090
                    self::checkMatrixOperands($arg, $multiplier, 2);
4091
4092
                    try {
4093
                        $matrix1 = new Shared\JAMA\Matrix($arg);
4094
                        $matrixResult = $matrix1->arrayTimesEquals($multiplier);
4095
                        $result = $matrixResult->getArray();
4096
                    } catch (\Exception $ex) {
4097
                        $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4098
                        $result = '#VALUE!';
4099
                    }
4100
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
4101
                    $stack->push('Value', $result);
4102
                    if (isset($storeKey)) {
4103
                        $branchStore[$storeKey] = $result;
4104
                    }
4105
                } else {
4106 4
                    $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack);
4107
                }
4108 185
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token, $matches)) {
4109 81
                $cellRef = null;
4110 81
                if (isset($matches[8])) {
4111
                    if ($pCell === null) {
4112
                        //                        We can't access the range, so return a REF error
4113
                        $cellValue = Functions::REF();
4114
                    } else {
4115
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
4116
                        if ($matches[2] > '') {
4117
                            $matches[2] = trim($matches[2], "\"'");
4118
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
4119
                                //    It's a Reference to an external spreadsheet (not currently supported)
4120
                                return $this->raiseFormulaError('Unable to access External Workbook');
4121
                            }
4122
                            $matches[2] = trim($matches[2], "\"'");
4123
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]);
4124
                            if ($pCellParent !== null) {
4125
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4126
                            } else {
4127
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4128
                            }
4129
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
4130
                        } else {
4131
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet');
4132
                            if ($pCellParent !== null) {
4133
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4134
                            } else {
4135
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4136
                            }
4137
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
4138
                        }
4139
                    }
4140
                } else {
4141 81
                    if ($pCell === null) {
4142
                        //                        We can't access the cell, so return a REF error
4143
                        $cellValue = Functions::REF();
4144
                    } else {
4145 81
                        $cellRef = $matches[6] . $matches[7];
4146 81
                        if ($matches[2] > '') {
4147 16
                            $matches[2] = trim($matches[2], "\"'");
4148 16
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
4149
                                //    It's a Reference to an external spreadsheet (not currently supported)
4150
                                return $this->raiseFormulaError('Unable to access External Workbook');
4151
                            }
4152 16
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]);
4153 16
                            if ($pCellParent !== null) {
4154 16
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
4155 16
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
4156 16
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4157 16
                                    $pCell->attach($pCellParent);
4158
                                } else {
4159 16
                                    $cellValue = null;
4160
                                }
4161
                            } else {
4162
                                return $this->raiseFormulaError('Unable to access Cell Reference');
4163
                            }
4164 16
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
4165
                        } else {
4166 68
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet');
4167 68
                            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

4167
                            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...
4168 67
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
4169 67
                                $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

4169
                                $pCell->attach(/** @scrutinizer ignore-type */ $pCellParent);
Loading history...
4170
                            } else {
4171 5
                                $cellValue = null;
4172
                            }
4173 68
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
4174
                        }
4175
                    }
4176
                }
4177 81
                $stack->push('Value', $cellValue, $cellRef);
4178 81
                if (isset($storeKey)) {
4179 81
                    $branchStore[$storeKey] = $cellValue;
4180
                }
4181
4182
                // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
4183 170
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) {
4184 79
                if ($pCellParent) {
4185 79
                    $pCell->attach($pCellParent);
4186
                }
4187
4188 79
                $functionName = $matches[1];
4189 79
                $argCount = $stack->pop();
4190 79
                $argCount = $argCount['value'];
4191 79
                if ($functionName != 'MKMATRIX') {
4192 79
                    $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's'));
4193
                }
4194 79
                if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) {    // function
4195 79
                    if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
4196 79
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
4197 79
                        $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
4198 79
                        $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
4199 6
                    } elseif (isset(self::$controlFunctions[$functionName])) {
4200 6
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
4201 6
                        $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
4202 6
                        $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
4203
                    }
4204
                    // get the arguments for this function
4205 79
                    $args = $argArrayVals = [];
4206 79
                    for ($i = 0; $i < $argCount; ++$i) {
4207 78
                        $arg = $stack->pop();
4208 78
                        $a = $argCount - $i - 1;
4209 78
                        if (($passByReference) &&
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $passByReference does not seem to be defined for all execution paths leading up to this point.
Loading history...
4210 78
                            (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) &&
4211 78
                            (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) {
4212 2
                            if ($arg['reference'] === null) {
4213
                                $args[] = $cellID;
4214
                                if ($functionName != 'MKMATRIX') {
4215
                                    $argArrayVals[] = $this->showValue($cellID);
4216
                                }
4217
                            } else {
4218 2
                                $args[] = $arg['reference'];
4219 2
                                if ($functionName != 'MKMATRIX') {
4220 2
                                    $argArrayVals[] = $this->showValue($arg['reference']);
4221
                                }
4222
                            }
4223
                        } else {
4224 77
                            $args[] = self::unwrapResult($arg['value']);
4225 77
                            if ($functionName != 'MKMATRIX') {
4226 77
                                $argArrayVals[] = $this->showValue($arg['value']);
4227
                            }
4228
                        }
4229
                    }
4230
4231
                    //    Reverse the order of the arguments
4232 79
                    krsort($args);
4233
4234 79
                    if (($passByReference) && ($argCount == 0)) {
4235 1
                        $args[] = $cellID;
4236 1
                        $argArrayVals[] = $this->showValue($cellID);
4237
                    }
4238
4239 79
                    if ($functionName != 'MKMATRIX') {
4240 79
                        if ($this->debugLog->getWriteDebugLog()) {
4241
                            krsort($argArrayVals);
4242
                            $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )');
4243
                        }
4244
                    }
4245
4246
                    //    Process the argument with the appropriate function call
4247 79
                    $args = $this->addCellReference($args, $passCellReference, $functionCall, $pCell);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $passCellReference does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $functionCall does not seem to be defined for all execution paths leading up to this point.
Loading history...
4248
4249 79
                    if (!is_array($functionCall)) {
4250 3
                        foreach ($args as &$arg) {
4251 2
                            $arg = Functions::flattenSingleValue($arg);
4252
                        }
4253 3
                        unset($arg);
4254
                    }
4255
4256 79
                    $result = call_user_func_array($functionCall, $args);
4257
4258 79
                    if ($functionName != 'MKMATRIX') {
4259 79
                        $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
4260
                    }
4261 79
                    $stack->push('Value', self::wrapResult($result));
4262 79
                    if (isset($storeKey)) {
4263 79
                        $branchStore[$storeKey] = $result;
4264
                    }
4265
                }
4266
            } else {
4267
                // if the token is a number, boolean, string or an Excel error, push it onto the stack
4268 170
                if (isset(self::$excelConstants[strtoupper($token)])) {
4269
                    $excelConstant = strtoupper($token);
4270
                    $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
4271
                    if (isset($storeKey)) {
4272
                        $branchStore[$storeKey] = self::$excelConstants[$excelConstant];
4273
                    }
4274
                    $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant]));
4275 170
                } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == '"') || ($token[0] == '#')) {
4276 169
                    $stack->push('Value', $token);
4277 169
                    if (isset($storeKey)) {
4278 169
                        $branchStore[$storeKey] = $token;
4279
                    }
4280
                    // if the token is a named range, push the named range name onto the stack
4281 10
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) {
4282 10
                    $namedRange = $matches[6];
4283 10
                    $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
4284
4285 10
                    $cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
4286 10
                    $pCell->attach($pCellParent);
4287 10
                    $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
4288 10
                    $stack->push('Named Range', $cellValue, $namedRange);
4289 10
                    if (isset($storeKey)) {
4290 10
                        $branchStore[$storeKey] = $cellValue;
4291
                    }
4292
                } else {
4293
                    return $this->raiseFormulaError("undefined variable '$token'");
4294
                }
4295
            }
4296
        }
4297
        // when we're out of tokens, the stack should have a single element, the final result
4298 185
        if ($stack->count() != 1) {
4299
            return $this->raiseFormulaError('internal error');
4300
        }
4301 185
        $output = $stack->pop();
4302 185
        $output = $output['value'];
4303
4304 185
        return $output;
4305
    }
4306
4307 39
    private function validateBinaryOperand(&$operand, &$stack)
4308
    {
4309 39
        if (is_array($operand)) {
4310 26
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
4311
                do {
4312 26
                    $operand = array_pop($operand);
4313 26
                } while (is_array($operand));
4314
            }
4315
        }
4316
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
4317 39
        if (is_string($operand)) {
4318
            //    We only need special validations for the operand if it is a string
4319
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
4320 2
            if ($operand > '' && $operand[0] == '"') {
4321
                $operand = self::unwrapResult($operand);
4322
            }
4323
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
4324 2
            if (!is_numeric($operand)) {
4325
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
4326 2
                if ($operand > '' && $operand[0] == '#') {
4327
                    $stack->push('Value', $operand);
4328
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand));
4329
4330
                    return false;
4331 2
                } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
4332
                    //    If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
4333 2
                    $stack->push('Value', '#VALUE!');
4334 2
                    $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!'));
4335
4336 2
                    return false;
4337
                }
4338
            }
4339
        }
4340
4341
        //    return a true if the value of the operand is one that we can use in normal binary operations
4342 38
        return true;
4343
    }
4344
4345
    /**
4346
     * @param null|string $cellID
4347
     * @param mixed $operand1
4348
     * @param mixed $operand2
4349
     * @param string $operation
4350
     * @param Stack $stack
4351
     * @param bool $recursingArrays
4352
     *
4353
     * @return mixed
4354
     */
4355 112
    private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
4356
    {
4357
        //    If we're dealing with matrix operations, we want a matrix result
4358 112
        if ((is_array($operand1)) || (is_array($operand2))) {
4359 19
            $result = [];
4360 19
            if ((is_array($operand1)) && (!is_array($operand2))) {
4361 17
                foreach ($operand1 as $x => $operandData) {
4362 17
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2));
4363 17
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack);
4364 17
                    $r = $stack->pop();
4365 17
                    $result[$x] = $r['value'];
4366
                }
4367 2
            } elseif ((!is_array($operand1)) && (is_array($operand2))) {
4368
                foreach ($operand2 as $x => $operandData) {
4369
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData));
4370
                    $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack);
4371
                    $r = $stack->pop();
4372
                    $result[$x] = $r['value'];
4373
                }
4374
            } else {
4375 2
                if (!$recursingArrays) {
4376 2
                    self::checkMatrixOperands($operand1, $operand2, 2);
4377
                }
4378 2
                foreach ($operand1 as $x => $operandData) {
4379 2
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x]));
4380 2
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true);
4381 2
                    $r = $stack->pop();
4382 2
                    $result[$x] = $r['value'];
4383
                }
4384
            }
4385
            //    Log the result details
4386 19
            $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result));
4387
            //    And push the result onto the stack
4388 19
            $stack->push('Array', $result);
4389
4390 19
            return $result;
4391
        }
4392
4393
        //    Simple validate the two operands if they are string values
4394 112
        if (is_string($operand1) && $operand1 > '' && $operand1[0] == '"') {
4395 56
            $operand1 = self::unwrapResult($operand1);
4396
        }
4397 112
        if (is_string($operand2) && $operand2 > '' && $operand2[0] == '"') {
4398 71
            $operand2 = self::unwrapResult($operand2);
4399
        }
4400
4401
        // Use case insensitive comparaison if not OpenOffice mode
4402 112
        if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
4403 112
            if (is_string($operand1)) {
4404 63
                $operand1 = strtoupper($operand1);
4405
            }
4406 112
            if (is_string($operand2)) {
4407 73
                $operand2 = strtoupper($operand2);
4408
            }
4409
        }
4410
4411 112
        $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE;
4412
4413
        //    execute the necessary operation
4414
        switch ($operation) {
4415
            //    Greater than
4416 112
            case '>':
4417 21
                if ($useLowercaseFirstComparison) {
4418 9
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0;
4419
                } else {
4420 21
                    $result = ($operand1 > $operand2);
4421
                }
4422
4423 21
                break;
4424
            //    Less than
4425 95
            case '<':
4426 13
                if ($useLowercaseFirstComparison) {
4427 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0;
4428
                } else {
4429 13
                    $result = ($operand1 < $operand2);
4430
                }
4431
4432 13
                break;
4433
            //    Equality
4434 83
            case '=':
4435 44
                if (is_numeric($operand1) && is_numeric($operand2)) {
4436 12
                    $result = (abs($operand1 - $operand2) < $this->delta);
4437
                } else {
4438 36
                    $result = strcmp($operand1, $operand2) == 0;
4439
                }
4440
4441 44
                break;
4442
            //    Greater than or equal
4443 39
            case '>=':
4444 9
                if (is_numeric($operand1) && is_numeric($operand2)) {
4445 5
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2));
4446 4
                } elseif ($useLowercaseFirstComparison) {
4447 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0;
4448
                } else {
4449 4
                    $result = strcmp($operand1, $operand2) >= 0;
4450
                }
4451
4452 9
                break;
4453
            //    Less than or equal
4454 30
            case '<=':
4455 11
                if (is_numeric($operand1) && is_numeric($operand2)) {
4456 7
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2));
4457 4
                } elseif ($useLowercaseFirstComparison) {
4458 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0;
4459
                } else {
4460 4
                    $result = strcmp($operand1, $operand2) <= 0;
4461
                }
4462
4463 11
                break;
4464
            //    Inequality
4465 19
            case '<>':
4466 19
                if (is_numeric($operand1) && is_numeric($operand2)) {
4467 3
                    $result = (abs($operand1 - $operand2) > 1E-14);
4468
                } else {
4469 16
                    $result = strcmp($operand1, $operand2) != 0;
4470
                }
4471
4472 19
                break;
4473
        }
4474
4475
        //    Log the result details
4476 112
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
4477
        //    And push the result onto the stack
4478 112
        $stack->push('Value', $result);
4479
4480 112
        return $result;
4481
    }
4482
4483
    /**
4484
     * Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
4485
     *
4486
     * @param string $str1 First string value for the comparison
4487
     * @param string $str2 Second string value for the comparison
4488
     *
4489
     * @return int
4490
     */
4491 21
    private function strcmpLowercaseFirst($str1, $str2)
4492
    {
4493 21
        $inversedStr1 = Shared\StringHelper::strCaseReverse($str1);
4494 21
        $inversedStr2 = Shared\StringHelper::strCaseReverse($str2);
4495
4496 21
        return strcmp($inversedStr1, $inversedStr2);
4497
    }
4498
4499
    /**
4500
     * @param mixed $operand1
4501
     * @param mixed $operand2
4502
     * @param mixed $operation
4503
     * @param string $matrixFunction
4504
     * @param mixed $stack
4505
     *
4506
     * @return bool|mixed
4507
     */
4508 39
    private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack)
4509
    {
4510
        //    Validate the two operands
4511 39
        if (!$this->validateBinaryOperand($operand1, $stack)) {
4512 2
            return false;
4513
        }
4514 38
        if (!$this->validateBinaryOperand($operand2, $stack)) {
4515 1
            return false;
4516
        }
4517
4518
        //    If either of the operands is a matrix, we need to treat them both as matrices
4519
        //        (converting the other operand to a matrix if need be); then perform the required
4520
        //        matrix operation
4521 38
        if ((is_array($operand1)) || (is_array($operand2))) {
4522
            //    Ensure that both operands are arrays/matrices of the same size
4523
            self::checkMatrixOperands($operand1, $operand2, 2);
4524
4525
            try {
4526
                //    Convert operand 1 from a PHP array to a matrix
4527
                $matrix = new Shared\JAMA\Matrix($operand1);
4528
                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4529
                $matrixResult = $matrix->$matrixFunction($operand2);
4530
                $result = $matrixResult->getArray();
4531
            } catch (\Exception $ex) {
4532
                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4533
                $result = '#VALUE!';
4534
            }
4535
        } else {
4536 38
            if ((Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) &&
4537 38
                ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) ||
4538 38
                 (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0))) {
4539
                $result = Functions::VALUE();
4540
            } else {
4541
                //    If we're dealing with non-matrix operations, execute the necessary operation
4542
                switch ($operation) {
4543
                    //    Addition
4544 38
                    case '+':
4545 26
                        $result = $operand1 + $operand2;
4546
4547 26
                        break;
4548
                    //    Subtraction
4549 29
                    case '-':
4550 7
                        $result = $operand1 - $operand2;
4551
4552 7
                        break;
4553
                    //    Multiplication
4554 27
                    case '*':
4555 22
                        $result = $operand1 * $operand2;
4556
4557 22
                        break;
4558
                    //    Division
4559 8
                    case '/':
4560 8
                        if ($operand2 == 0) {
4561
                            //    Trap for Divide by Zero error
4562 8
                            $stack->push('Value', '#DIV/0!');
4563 8
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails('#DIV/0!'));
4564
4565 8
                            return false;
4566
                        }
4567
                            $result = $operand1 / $operand2;
4568
4569
                        break;
4570
                    //    Power
4571
                    case '^':
4572
                        $result = pow($operand1, $operand2);
4573
4574
                        break;
4575
                }
4576
            }
4577
        }
4578
4579
        //    Log the result details
4580 33
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
4581
        //    And push the result onto the stack
4582 33
        $stack->push('Value', $result);
4583
4584 33
        return $result;
4585
    }
4586
4587
    // trigger an error, but nicely, if need be
4588
    protected function raiseFormulaError($errorMessage)
4589
    {
4590
        $this->formulaError = $errorMessage;
4591
        $this->cyclicReferenceStack->clear();
4592
        if (!$this->suppressFormulaErrors) {
4593
            throw new Exception($errorMessage);
4594
        }
4595
        trigger_error($errorMessage, E_USER_ERROR);
4596
4597
        return false;
4598
    }
4599
4600
    /**
4601
     * Extract range values.
4602
     *
4603
     * @param string &$pRange String based range representation
4604
     * @param Worksheet $pSheet Worksheet
4605
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4606
     *
4607
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4608
     */
4609 82
    public function extractCellRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4610
    {
4611
        // Return value
4612 82
        $returnValue = [];
4613
4614 82
        if ($pSheet !== null) {
4615 82
            $pSheetName = $pSheet->getTitle();
4616 82
            if (strpos($pRange, '!') !== false) {
4617
                [$pSheetName, $pRange] = Worksheet::extractSheetTitle($pRange, true);
4618
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4619
            }
4620
4621
            // Extract range
4622 82
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4623 82
            $pRange = $pSheetName . '!' . $pRange;
4624 82
            if (!isset($aReferences[1])) {
4625 82
                $currentCol = '';
4626 82
                $currentRow = 0;
4627
                //    Single cell in range
4628 82
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
4629 82
                if ($pSheet->cellExists($aReferences[0])) {
4630 82
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4631
                } else {
4632 82
                    $returnValue[$currentRow][$currentCol] = null;
4633
                }
4634
            } else {
4635
                // Extract cell data for all cells in the range
4636 58
                foreach ($aReferences as $reference) {
4637 58
                    $currentCol = '';
4638 58
                    $currentRow = 0;
4639
                    // Extract range
4640 58
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
4641 58
                    if ($pSheet->cellExists($reference)) {
4642 58
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4643
                    } else {
4644 14
                        $returnValue[$currentRow][$currentCol] = null;
4645
                    }
4646
                }
4647
            }
4648
        }
4649
4650 82
        return $returnValue;
4651
    }
4652
4653
    /**
4654
     * Extract range values.
4655
     *
4656
     * @param string &$pRange String based range representation
4657
     * @param Worksheet $pSheet Worksheet
4658
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4659
     *
4660
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4661
     */
4662 10
    public function extractNamedRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4663
    {
4664
        // Return value
4665 10
        $returnValue = [];
4666
4667 10
        if ($pSheet !== null) {
4668 10
            $pSheetName = $pSheet->getTitle();
0 ignored issues
show
Unused Code introduced by
The assignment to $pSheetName is dead and can be removed.
Loading history...
4669 10
            if (strpos($pRange, '!') !== false) {
4670
                [$pSheetName, $pRange] = Worksheet::extractSheetTitle($pRange, true);
4671
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4672
            }
4673
4674
            // Named range?
4675 10
            $namedRange = NamedRange::resolveRange($pRange, $pSheet);
1 ignored issue
show
Bug introduced by
It seems like $pSheet can also be of type null; however, parameter $pSheet of PhpOffice\PhpSpreadsheet...edRange::resolveRange() 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

4675
            $namedRange = NamedRange::resolveRange($pRange, /** @scrutinizer ignore-type */ $pSheet);
Loading history...
4676 10
            if ($namedRange !== null) {
4677 7
                $pSheet = $namedRange->getWorksheet();
4678 7
                $pRange = $namedRange->getRange();
4679 7
                $splitRange = Coordinate::splitRange($pRange);
4680
                //    Convert row and column references
4681 7
                if (ctype_alpha($splitRange[0][0])) {
4682
                    $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
4683 7
                } elseif (ctype_digit($splitRange[0][0])) {
4684 7
                    $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
4685
                }
4686
            } else {
4687 3
                return Functions::REF();
4688
            }
4689
4690
            // Extract range
4691 7
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4692 7
            if (!isset($aReferences[1])) {
4693
                //    Single cell (or single column or row) in range
4694 6
                [$currentCol, $currentRow] = Coordinate::coordinateFromString($aReferences[0]);
4695 6
                if ($pSheet->cellExists($aReferences[0])) {
4696 6
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4697
                } else {
4698 6
                    $returnValue[$currentRow][$currentCol] = null;
4699
                }
4700
            } else {
4701
                // Extract cell data for all cells in the range
4702 1
                foreach ($aReferences as $reference) {
4703
                    // Extract range
4704 1
                    [$currentCol, $currentRow] = Coordinate::coordinateFromString($reference);
4705 1
                    if ($pSheet->cellExists($reference)) {
4706 1
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4707
                    } else {
4708
                        $returnValue[$currentRow][$currentCol] = null;
4709
                    }
4710
                }
4711
            }
4712
        }
4713
4714 7
        return $returnValue;
4715
    }
4716
4717
    /**
4718
     * Is a specific function implemented?
4719
     *
4720
     * @param string $pFunction Function Name
4721
     *
4722
     * @return bool
4723
     */
4724 3
    public function isImplemented($pFunction)
4725
    {
4726 3
        $pFunction = strtoupper($pFunction);
4727 3
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
4728
4729 3
        return !$notImplemented;
4730
    }
4731
4732
    /**
4733
     * Get a list of all implemented functions as an array of function objects.
4734
     *
4735
     * @return array of Category
4736
     */
4737
    public function getFunctions()
4738
    {
4739
        return self::$phpSpreadsheetFunctions;
4740
    }
4741
4742
    /**
4743
     * Get a list of implemented Excel function names.
4744
     *
4745
     * @return array
4746
     */
4747 2
    public function getImplementedFunctionNames()
4748
    {
4749 2
        $returnValue = [];
4750 2
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
4751 2
            if ($this->isImplemented($functionName)) {
4752 2
                $returnValue[] = $functionName;
4753
            }
4754
        }
4755
4756 2
        return $returnValue;
4757
    }
4758
4759
    /**
4760
     * Add cell reference if needed while making sure that it is the last argument.
4761
     *
4762
     * @param array $args
4763
     * @param bool $passCellReference
4764
     * @param array|string $functionCall
4765
     * @param null|Cell $pCell
4766
     *
4767
     * @return array
4768
     */
4769 79
    private function addCellReference(array $args, $passCellReference, $functionCall, Cell $pCell = null)
4770
    {
4771 79
        if ($passCellReference) {
4772 4
            if (is_array($functionCall)) {
4773 4
                $className = $functionCall[0];
4774 4
                $methodName = $functionCall[1];
4775
4776 4
                $reflectionMethod = new \ReflectionMethod($className, $methodName);
4777 4
                $argumentCount = count($reflectionMethod->getParameters());
4778 4
                while (count($args) < $argumentCount - 1) {
4779 1
                    $args[] = null;
4780
                }
4781
            }
4782
4783 4
            $args[] = $pCell;
4784
        }
4785
4786 79
        return $args;
4787
    }
4788
4789 27
    private function getUnusedBranchStoreKey()
4790
    {
4791 27
        $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter;
4792 27
        ++$this->branchStoreKeyCounter;
4793
4794 27
        return $storeKeyValue;
4795
    }
4796
4797
    private function getTokensAsString($tokens)
4798
    {
4799
        $tokensStr = array_map(function ($token) {
4800
            $value = $token['value'] ?? 'no value';
4801
            while (is_array($value)) {
4802
                $value = array_pop($value);
4803
            }
4804
4805
            return $value;
4806
        }, $tokens);
4807
4808
        return '[ ' . implode(' | ', $tokensStr) . ' ]';
4809
    }
4810
}
4811