Failed Conditions
Push — develop ( 5b3870...6088f5 )
by Adrien
35:14 queued 30:08
created

Calculation::clearCalculationCacheForWorksheet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.1481

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 4
ccs 2
cts 3
cp 0.6667
crap 2.1481
rs 10
c 0
b 0
f 0
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,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?([a-z]{1,3})\$?(\d{1,7})';
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
     * List of operators that can be used within formulae
71
     * The true/false value indicates whether it is a binary operator or a unary operator.
72
     *
73
     * @var array
74
     */
75
    private static $operators = [
76
        '+' => true, '-' => true, '*' => true, '/' => true,
77
        '^' => true, '&' => true, '%' => false, '~' => false,
78
        '>' => true, '<' => true, '=' => true, '>=' => true,
79
        '<=' => true, '<>' => true, '|' => true, ':' => true,
80
    ];
81
82
    /**
83
     * List of binary operators (those that expect two operands).
84
     *
85
     * @var array
86
     */
87
    private static $binaryOperators = [
88
        '+' => true, '-' => true, '*' => true, '/' => true,
89
        '^' => true, '&' => true, '>' => true, '<' => true,
90
        '=' => true, '>=' => true, '<=' => true, '<>' => true,
91
        '|' => true, ':' => true,
92
    ];
93
94
    /**
95
     * The debug log generated by the calculation engine.
96
     *
97
     * @var Logger
98
     */
99
    private $debugLog;
100
101
    /**
102
     * Flag to determine how formula errors should be handled
103
     *        If true, then a user error will be triggered
104
     *        If false, then an exception will be thrown.
105
     *
106
     * @var bool
107
     */
108
    public $suppressFormulaErrors = false;
109
110
    /**
111
     * Error message for any error that was raised/thrown by the calculation engine.
112
     *
113
     * @var string
114
     */
115
    public $formulaError;
116
117
    /**
118
     * An array of the nested cell references accessed by the calculation engine, used for the debug log.
119
     *
120
     * @var array of string
121
     */
122
    private $cyclicReferenceStack;
123
124
    private $cellStack = [];
125
126
    /**
127
     * Current iteration counter for cyclic formulae
128
     * If the value is 0 (or less) then cyclic formulae will throw an exception,
129
     * otherwise they will iterate to the limit defined here before returning a result.
130
     *
131
     * @var int
132
     */
133
    private $cyclicFormulaCounter = 1;
134
135
    private $cyclicFormulaCell = '';
136
137
    /**
138
     * Number of iterations for cyclic formulae.
139
     *
140
     * @var int
141
     */
142
    public $cyclicFormulaCount = 1;
143
144
    /**
145
     * Epsilon Precision used for comparisons in calculations.
146
     *
147
     * @var float
148
     */
149
    private $delta = 0.1e-12;
150
151
    /**
152
     * The current locale setting.
153
     *
154
     * @var string
155
     */
156
    private static $localeLanguage = 'en_us'; //    US English    (default locale)
157
158
    /**
159
     * List of available locale settings
160
     * Note that this is read for the locale subdirectory only when requested.
161
     *
162
     * @var string[]
163
     */
164
    private static $validLocaleLanguages = [
165
        'en', //    English        (default language)
166
    ];
167
168
    /**
169
     * Locale-specific argument separator for function arguments.
170
     *
171
     * @var string
172
     */
173
    private static $localeArgumentSeparator = ',';
174
175
    private static $localeFunctions = [];
176
177
    /**
178
     * Locale-specific translations for Excel constants (True, False and Null).
179
     *
180
     * @var string[]
181
     */
182
    public static $localeBoolean = [
183
        'TRUE' => 'TRUE',
184
        'FALSE' => 'FALSE',
185
        'NULL' => 'NULL',
186
    ];
187
188
    /**
189
     * Excel constant string translations to their PHP equivalents
190
     * Constant conversion from text name/value to actual (datatyped) value.
191
     *
192
     * @var string[]
193
     */
194
    private static $excelConstants = [
195
        'TRUE' => true,
196
        'FALSE' => false,
197
        'NULL' => null,
198
    ];
199
200
    // PhpSpreadsheet functions
201
    private static $phpSpreadsheetFunctions = [
202
        'ABS' => [
203
            'category' => Category::CATEGORY_MATH_AND_TRIG,
204
            'functionCall' => 'abs',
205
            'argumentCount' => '1',
206
        ],
207
        'ACCRINT' => [
208
            'category' => Category::CATEGORY_FINANCIAL,
209
            'functionCall' => [Financial::class, 'ACCRINT'],
210
            'argumentCount' => '4-7',
211
        ],
212
        'ACCRINTM' => [
213
            'category' => Category::CATEGORY_FINANCIAL,
214
            'functionCall' => [Financial::class, 'ACCRINTM'],
215
            'argumentCount' => '3-5',
216
        ],
217
        'ACOS' => [
218
            'category' => Category::CATEGORY_MATH_AND_TRIG,
219
            'functionCall' => 'acos',
220
            'argumentCount' => '1',
221
        ],
222
        'ACOSH' => [
223
            'category' => Category::CATEGORY_MATH_AND_TRIG,
224
            'functionCall' => 'acosh',
225
            'argumentCount' => '1',
226
        ],
227
        'ACOT' => [
228
            'category' => Category::CATEGORY_MATH_AND_TRIG,
229
            'functionCall' => [MathTrig::class, 'ACOT'],
230
            'argumentCount' => '1',
231
        ],
232
        'ACOTH' => [
233
            'category' => Category::CATEGORY_MATH_AND_TRIG,
234
            'functionCall' => [MathTrig::class, 'ACOTH'],
235
            'argumentCount' => '1',
236
        ],
237
        'ADDRESS' => [
238
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
239
            'functionCall' => [LookupRef::class, 'cellAddress'],
240
            'argumentCount' => '2-5',
241
        ],
242
        'AMORDEGRC' => [
243
            'category' => Category::CATEGORY_FINANCIAL,
244
            'functionCall' => [Financial::class, 'AMORDEGRC'],
245
            'argumentCount' => '6,7',
246
        ],
247
        'AMORLINC' => [
248
            'category' => Category::CATEGORY_FINANCIAL,
249
            'functionCall' => [Financial::class, 'AMORLINC'],
250
            'argumentCount' => '6,7',
251
        ],
252
        'AND' => [
253
            'category' => Category::CATEGORY_LOGICAL,
254
            'functionCall' => [Logical::class, 'logicalAnd'],
255
            'argumentCount' => '1+',
256
        ],
257
        'AREAS' => [
258
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
259
            'functionCall' => [Functions::class, 'DUMMY'],
260
            'argumentCount' => '1',
261
        ],
262
        'ASC' => [
263
            'category' => Category::CATEGORY_TEXT_AND_DATA,
264
            'functionCall' => [Functions::class, 'DUMMY'],
265
            'argumentCount' => '1',
266
        ],
267
        'ASIN' => [
268
            'category' => Category::CATEGORY_MATH_AND_TRIG,
269
            'functionCall' => 'asin',
270
            'argumentCount' => '1',
271
        ],
272
        'ASINH' => [
273
            'category' => Category::CATEGORY_MATH_AND_TRIG,
274
            'functionCall' => 'asinh',
275
            'argumentCount' => '1',
276
        ],
277
        'ATAN' => [
278
            'category' => Category::CATEGORY_MATH_AND_TRIG,
279
            'functionCall' => 'atan',
280
            'argumentCount' => '1',
281
        ],
282
        'ATAN2' => [
283
            'category' => Category::CATEGORY_MATH_AND_TRIG,
284
            'functionCall' => [MathTrig::class, 'ATAN2'],
285
            'argumentCount' => '2',
286
        ],
287
        'ATANH' => [
288
            'category' => Category::CATEGORY_MATH_AND_TRIG,
289
            'functionCall' => 'atanh',
290
            'argumentCount' => '1',
291
        ],
292
        'AVEDEV' => [
293
            'category' => Category::CATEGORY_STATISTICAL,
294
            'functionCall' => [Statistical::class, 'AVEDEV'],
295
            'argumentCount' => '1+',
296
        ],
297
        'AVERAGE' => [
298
            'category' => Category::CATEGORY_STATISTICAL,
299
            'functionCall' => [Statistical::class, 'AVERAGE'],
300
            'argumentCount' => '1+',
301
        ],
302
        'AVERAGEA' => [
303
            'category' => Category::CATEGORY_STATISTICAL,
304
            'functionCall' => [Statistical::class, 'AVERAGEA'],
305
            'argumentCount' => '1+',
306
        ],
307
        'AVERAGEIF' => [
308
            'category' => Category::CATEGORY_STATISTICAL,
309
            'functionCall' => [Statistical::class, 'AVERAGEIF'],
310
            'argumentCount' => '2,3',
311
        ],
312
        'AVERAGEIFS' => [
313
            'category' => Category::CATEGORY_STATISTICAL,
314
            'functionCall' => [Functions::class, 'DUMMY'],
315
            'argumentCount' => '3+',
316
        ],
317
        'BAHTTEXT' => [
318
            'category' => Category::CATEGORY_TEXT_AND_DATA,
319
            'functionCall' => [Functions::class, 'DUMMY'],
320
            'argumentCount' => '1',
321
        ],
322
        'BESSELI' => [
323
            'category' => Category::CATEGORY_ENGINEERING,
324
            'functionCall' => [Engineering::class, 'BESSELI'],
325
            'argumentCount' => '2',
326
        ],
327
        'BESSELJ' => [
328
            'category' => Category::CATEGORY_ENGINEERING,
329
            'functionCall' => [Engineering::class, 'BESSELJ'],
330
            'argumentCount' => '2',
331
        ],
332
        'BESSELK' => [
333
            'category' => Category::CATEGORY_ENGINEERING,
334
            'functionCall' => [Engineering::class, 'BESSELK'],
335
            'argumentCount' => '2',
336
        ],
337
        'BESSELY' => [
338
            'category' => Category::CATEGORY_ENGINEERING,
339
            'functionCall' => [Engineering::class, 'BESSELY'],
340
            'argumentCount' => '2',
341
        ],
342
        'BETADIST' => [
343
            'category' => Category::CATEGORY_STATISTICAL,
344
            'functionCall' => [Statistical::class, 'BETADIST'],
345
            'argumentCount' => '3-5',
346
        ],
347
        'BETAINV' => [
348
            'category' => Category::CATEGORY_STATISTICAL,
349
            'functionCall' => [Statistical::class, 'BETAINV'],
350
            'argumentCount' => '3-5',
351
        ],
352
        'BIN2DEC' => [
353
            'category' => Category::CATEGORY_ENGINEERING,
354
            'functionCall' => [Engineering::class, 'BINTODEC'],
355
            'argumentCount' => '1',
356
        ],
357
        'BIN2HEX' => [
358
            'category' => Category::CATEGORY_ENGINEERING,
359
            'functionCall' => [Engineering::class, 'BINTOHEX'],
360
            'argumentCount' => '1,2',
361
        ],
362
        'BIN2OCT' => [
363
            'category' => Category::CATEGORY_ENGINEERING,
364
            'functionCall' => [Engineering::class, 'BINTOOCT'],
365
            'argumentCount' => '1,2',
366
        ],
367
        'BINOMDIST' => [
368
            'category' => Category::CATEGORY_STATISTICAL,
369
            'functionCall' => [Statistical::class, 'BINOMDIST'],
370
            'argumentCount' => '4',
371
        ],
372
        'BITAND' => [
373
            'category' => Category::CATEGORY_ENGINEERING,
374
            'functionCall' => [Engineering::class, 'BITAND'],
375
            'argumentCount' => '2',
376
        ],
377
        'BITOR' => [
378
            'category' => Category::CATEGORY_ENGINEERING,
379
            'functionCall' => [Engineering::class, 'BITOR'],
380
            'argumentCount' => '2',
381
        ],
382
        'BITXOR' => [
383
            'category' => Category::CATEGORY_ENGINEERING,
384
            'functionCall' => [Engineering::class, 'BITOR'],
385
            'argumentCount' => '2',
386
        ],
387
        'BITLSHIFT' => [
388
            'category' => Category::CATEGORY_ENGINEERING,
389
            'functionCall' => [Engineering::class, 'BITLSHIFT'],
390
            'argumentCount' => '2',
391
        ],
392
        'BITRSHIFT' => [
393
            'category' => Category::CATEGORY_ENGINEERING,
394
            'functionCall' => [Engineering::class, 'BITRSHIFT'],
395
            'argumentCount' => '2',
396
        ],
397
        'CEILING' => [
398
            'category' => Category::CATEGORY_MATH_AND_TRIG,
399
            'functionCall' => [MathTrig::class, 'CEILING'],
400
            'argumentCount' => '2',
401
        ],
402
        'CELL' => [
403
            'category' => Category::CATEGORY_INFORMATION,
404
            'functionCall' => [Functions::class, 'DUMMY'],
405
            'argumentCount' => '1,2',
406
        ],
407
        'CHAR' => [
408
            'category' => Category::CATEGORY_TEXT_AND_DATA,
409
            'functionCall' => [TextData::class, 'CHARACTER'],
410
            'argumentCount' => '1',
411
        ],
412
        'CHIDIST' => [
413
            'category' => Category::CATEGORY_STATISTICAL,
414
            'functionCall' => [Statistical::class, 'CHIDIST'],
415
            'argumentCount' => '2',
416
        ],
417
        'CHIINV' => [
418
            'category' => Category::CATEGORY_STATISTICAL,
419
            'functionCall' => [Statistical::class, 'CHIINV'],
420
            'argumentCount' => '2',
421
        ],
422
        'CHITEST' => [
423
            'category' => Category::CATEGORY_STATISTICAL,
424
            'functionCall' => [Functions::class, 'DUMMY'],
425
            'argumentCount' => '2',
426
        ],
427
        'CHOOSE' => [
428
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
429
            'functionCall' => [LookupRef::class, 'CHOOSE'],
430
            'argumentCount' => '2+',
431
        ],
432
        'CLEAN' => [
433
            'category' => Category::CATEGORY_TEXT_AND_DATA,
434
            'functionCall' => [TextData::class, 'TRIMNONPRINTABLE'],
435
            'argumentCount' => '1',
436
        ],
437
        'CODE' => [
438
            'category' => Category::CATEGORY_TEXT_AND_DATA,
439
            'functionCall' => [TextData::class, 'ASCIICODE'],
440
            'argumentCount' => '1',
441
        ],
442
        'COLUMN' => [
443
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
444
            'functionCall' => [LookupRef::class, 'COLUMN'],
445
            'argumentCount' => '-1',
446
            'passByReference' => [true],
447
        ],
448
        'COLUMNS' => [
449
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
450
            'functionCall' => [LookupRef::class, 'COLUMNS'],
451
            'argumentCount' => '1',
452
        ],
453
        'COMBIN' => [
454
            'category' => Category::CATEGORY_MATH_AND_TRIG,
455
            'functionCall' => [MathTrig::class, 'COMBIN'],
456
            'argumentCount' => '2',
457
        ],
458
        'COMPLEX' => [
459
            'category' => Category::CATEGORY_ENGINEERING,
460
            'functionCall' => [Engineering::class, 'COMPLEX'],
461
            'argumentCount' => '2,3',
462
        ],
463
        'CONCAT' => [
464
            'category' => Category::CATEGORY_TEXT_AND_DATA,
465
            'functionCall' => [TextData::class, 'CONCATENATE'],
466
            'argumentCount' => '1+',
467
        ],
468
        'CONCATENATE' => [
469
            'category' => Category::CATEGORY_TEXT_AND_DATA,
470
            'functionCall' => [TextData::class, 'CONCATENATE'],
471
            'argumentCount' => '1+',
472
        ],
473
        'CONFIDENCE' => [
474
            'category' => Category::CATEGORY_STATISTICAL,
475
            'functionCall' => [Statistical::class, 'CONFIDENCE'],
476
            'argumentCount' => '3',
477
        ],
478
        'CONVERT' => [
479
            'category' => Category::CATEGORY_ENGINEERING,
480
            'functionCall' => [Engineering::class, 'CONVERTUOM'],
481
            'argumentCount' => '3',
482
        ],
483
        'CORREL' => [
484
            'category' => Category::CATEGORY_STATISTICAL,
485
            'functionCall' => [Statistical::class, 'CORREL'],
486
            'argumentCount' => '2',
487
        ],
488
        'COS' => [
489
            'category' => Category::CATEGORY_MATH_AND_TRIG,
490
            'functionCall' => 'cos',
491
            'argumentCount' => '1',
492
        ],
493
        'COSH' => [
494
            'category' => Category::CATEGORY_MATH_AND_TRIG,
495
            'functionCall' => 'cosh',
496
            'argumentCount' => '1',
497
        ],
498
        'COT' => [
499
            'category' => Category::CATEGORY_MATH_AND_TRIG,
500
            'functionCall' => [MathTrig::class, 'COT'],
501
            'argumentCount' => '1',
502
        ],
503
        'COTH' => [
504
            'category' => Category::CATEGORY_MATH_AND_TRIG,
505
            'functionCall' => [MathTrig::class, 'COTH'],
506
            'argumentCount' => '1',
507
        ],
508
        'COUNT' => [
509
            'category' => Category::CATEGORY_STATISTICAL,
510
            'functionCall' => [Statistical::class, 'COUNT'],
511
            'argumentCount' => '1+',
512
        ],
513
        'COUNTA' => [
514
            'category' => Category::CATEGORY_STATISTICAL,
515
            'functionCall' => [Statistical::class, 'COUNTA'],
516
            'argumentCount' => '1+',
517
        ],
518
        'COUNTBLANK' => [
519
            'category' => Category::CATEGORY_STATISTICAL,
520
            'functionCall' => [Statistical::class, 'COUNTBLANK'],
521
            'argumentCount' => '1',
522
        ],
523
        'COUNTIF' => [
524
            'category' => Category::CATEGORY_STATISTICAL,
525
            'functionCall' => [Statistical::class, 'COUNTIF'],
526
            'argumentCount' => '2',
527
        ],
528
        'COUNTIFS' => [
529
            'category' => Category::CATEGORY_STATISTICAL,
530
            'functionCall' => [Functions::class, 'DUMMY'],
531
            'argumentCount' => '2',
532
        ],
533
        'COUPDAYBS' => [
534
            'category' => Category::CATEGORY_FINANCIAL,
535
            'functionCall' => [Financial::class, 'COUPDAYBS'],
536
            'argumentCount' => '3,4',
537
        ],
538
        'COUPDAYS' => [
539
            'category' => Category::CATEGORY_FINANCIAL,
540
            'functionCall' => [Financial::class, 'COUPDAYS'],
541
            'argumentCount' => '3,4',
542
        ],
543
        'COUPDAYSNC' => [
544
            'category' => Category::CATEGORY_FINANCIAL,
545
            'functionCall' => [Financial::class, 'COUPDAYSNC'],
546
            'argumentCount' => '3,4',
547
        ],
548
        'COUPNCD' => [
549
            'category' => Category::CATEGORY_FINANCIAL,
550
            'functionCall' => [Financial::class, 'COUPNCD'],
551
            'argumentCount' => '3,4',
552
        ],
553
        'COUPNUM' => [
554
            'category' => Category::CATEGORY_FINANCIAL,
555
            'functionCall' => [Financial::class, 'COUPNUM'],
556
            'argumentCount' => '3,4',
557
        ],
558
        'COUPPCD' => [
559
            'category' => Category::CATEGORY_FINANCIAL,
560
            'functionCall' => [Financial::class, 'COUPPCD'],
561
            'argumentCount' => '3,4',
562
        ],
563
        'COVAR' => [
564
            'category' => Category::CATEGORY_STATISTICAL,
565
            'functionCall' => [Statistical::class, 'COVAR'],
566
            'argumentCount' => '2',
567
        ],
568
        'CRITBINOM' => [
569
            'category' => Category::CATEGORY_STATISTICAL,
570
            'functionCall' => [Statistical::class, 'CRITBINOM'],
571
            'argumentCount' => '3',
572
        ],
573
        'CSC' => [
574
            'category' => Category::CATEGORY_MATH_AND_TRIG,
575
            'functionCall' => [MathTrig::class, 'CSC'],
576
            'argumentCount' => '1',
577
        ],
578
        'CSCH' => [
579
            'category' => Category::CATEGORY_MATH_AND_TRIG,
580
            'functionCall' => [MathTrig::class, 'CSCH'],
581
            'argumentCount' => '1',
582
        ],
583
        'CUBEKPIMEMBER' => [
584
            'category' => Category::CATEGORY_CUBE,
585
            'functionCall' => [Functions::class, 'DUMMY'],
586
            'argumentCount' => '?',
587
        ],
588
        'CUBEMEMBER' => [
589
            'category' => Category::CATEGORY_CUBE,
590
            'functionCall' => [Functions::class, 'DUMMY'],
591
            'argumentCount' => '?',
592
        ],
593
        'CUBEMEMBERPROPERTY' => [
594
            'category' => Category::CATEGORY_CUBE,
595
            'functionCall' => [Functions::class, 'DUMMY'],
596
            'argumentCount' => '?',
597
        ],
598
        'CUBERANKEDMEMBER' => [
599
            'category' => Category::CATEGORY_CUBE,
600
            'functionCall' => [Functions::class, 'DUMMY'],
601
            'argumentCount' => '?',
602
        ],
603
        'CUBESET' => [
604
            'category' => Category::CATEGORY_CUBE,
605
            'functionCall' => [Functions::class, 'DUMMY'],
606
            'argumentCount' => '?',
607
        ],
608
        'CUBESETCOUNT' => [
609
            'category' => Category::CATEGORY_CUBE,
610
            'functionCall' => [Functions::class, 'DUMMY'],
611
            'argumentCount' => '?',
612
        ],
613
        'CUBEVALUE' => [
614
            'category' => Category::CATEGORY_CUBE,
615
            'functionCall' => [Functions::class, 'DUMMY'],
616
            'argumentCount' => '?',
617
        ],
618
        'CUMIPMT' => [
619
            'category' => Category::CATEGORY_FINANCIAL,
620
            'functionCall' => [Financial::class, 'CUMIPMT'],
621
            'argumentCount' => '6',
622
        ],
623
        'CUMPRINC' => [
624
            'category' => Category::CATEGORY_FINANCIAL,
625
            'functionCall' => [Financial::class, 'CUMPRINC'],
626
            'argumentCount' => '6',
627
        ],
628
        'DATE' => [
629
            'category' => Category::CATEGORY_DATE_AND_TIME,
630
            'functionCall' => [DateTime::class, 'DATE'],
631
            'argumentCount' => '3',
632
        ],
633
        'DATEDIF' => [
634
            'category' => Category::CATEGORY_DATE_AND_TIME,
635
            'functionCall' => [DateTime::class, 'DATEDIF'],
636
            'argumentCount' => '2,3',
637
        ],
638
        'DATEVALUE' => [
639
            'category' => Category::CATEGORY_DATE_AND_TIME,
640
            'functionCall' => [DateTime::class, 'DATEVALUE'],
641
            'argumentCount' => '1',
642
        ],
643
        'DAVERAGE' => [
644
            'category' => Category::CATEGORY_DATABASE,
645
            'functionCall' => [Database::class, 'DAVERAGE'],
646
            'argumentCount' => '3',
647
        ],
648
        'DAY' => [
649
            'category' => Category::CATEGORY_DATE_AND_TIME,
650
            'functionCall' => [DateTime::class, 'DAYOFMONTH'],
651
            'argumentCount' => '1',
652
        ],
653
        'DAYS' => [
654
            'category' => Category::CATEGORY_DATE_AND_TIME,
655
            'functionCall' => [DateTime::class, 'DAYS'],
656
            'argumentCount' => '2',
657
        ],
658
        'DAYS360' => [
659
            'category' => Category::CATEGORY_DATE_AND_TIME,
660
            'functionCall' => [DateTime::class, 'DAYS360'],
661
            'argumentCount' => '2,3',
662
        ],
663
        'DB' => [
664
            'category' => Category::CATEGORY_FINANCIAL,
665
            'functionCall' => [Financial::class, 'DB'],
666
            'argumentCount' => '4,5',
667
        ],
668
        'DCOUNT' => [
669
            'category' => Category::CATEGORY_DATABASE,
670
            'functionCall' => [Database::class, 'DCOUNT'],
671
            'argumentCount' => '3',
672
        ],
673
        'DCOUNTA' => [
674
            'category' => Category::CATEGORY_DATABASE,
675
            'functionCall' => [Database::class, 'DCOUNTA'],
676
            'argumentCount' => '3',
677
        ],
678
        'DDB' => [
679
            'category' => Category::CATEGORY_FINANCIAL,
680
            'functionCall' => [Financial::class, 'DDB'],
681
            'argumentCount' => '4,5',
682
        ],
683
        'DEC2BIN' => [
684
            'category' => Category::CATEGORY_ENGINEERING,
685
            'functionCall' => [Engineering::class, 'DECTOBIN'],
686
            'argumentCount' => '1,2',
687
        ],
688
        'DEC2HEX' => [
689
            'category' => Category::CATEGORY_ENGINEERING,
690
            'functionCall' => [Engineering::class, 'DECTOHEX'],
691
            'argumentCount' => '1,2',
692
        ],
693
        'DEC2OCT' => [
694
            'category' => Category::CATEGORY_ENGINEERING,
695
            'functionCall' => [Engineering::class, 'DECTOOCT'],
696
            'argumentCount' => '1,2',
697
        ],
698
        'DEGREES' => [
699
            'category' => Category::CATEGORY_MATH_AND_TRIG,
700
            'functionCall' => 'rad2deg',
701
            'argumentCount' => '1',
702
        ],
703
        'DELTA' => [
704
            'category' => Category::CATEGORY_ENGINEERING,
705
            'functionCall' => [Engineering::class, 'DELTA'],
706
            'argumentCount' => '1,2',
707
        ],
708
        'DEVSQ' => [
709
            'category' => Category::CATEGORY_STATISTICAL,
710
            'functionCall' => [Statistical::class, 'DEVSQ'],
711
            'argumentCount' => '1+',
712
        ],
713
        'DGET' => [
714
            'category' => Category::CATEGORY_DATABASE,
715
            'functionCall' => [Database::class, 'DGET'],
716
            'argumentCount' => '3',
717
        ],
718
        'DISC' => [
719
            'category' => Category::CATEGORY_FINANCIAL,
720
            'functionCall' => [Financial::class, 'DISC'],
721
            'argumentCount' => '4,5',
722
        ],
723
        'DMAX' => [
724
            'category' => Category::CATEGORY_DATABASE,
725
            'functionCall' => [Database::class, 'DMAX'],
726
            'argumentCount' => '3',
727
        ],
728
        'DMIN' => [
729
            'category' => Category::CATEGORY_DATABASE,
730
            'functionCall' => [Database::class, 'DMIN'],
731
            'argumentCount' => '3',
732
        ],
733
        'DOLLAR' => [
734
            'category' => Category::CATEGORY_TEXT_AND_DATA,
735
            'functionCall' => [TextData::class, 'DOLLAR'],
736
            'argumentCount' => '1,2',
737
        ],
738
        'DOLLARDE' => [
739
            'category' => Category::CATEGORY_FINANCIAL,
740
            'functionCall' => [Financial::class, 'DOLLARDE'],
741
            'argumentCount' => '2',
742
        ],
743
        'DOLLARFR' => [
744
            'category' => Category::CATEGORY_FINANCIAL,
745
            'functionCall' => [Financial::class, 'DOLLARFR'],
746
            'argumentCount' => '2',
747
        ],
748
        'DPRODUCT' => [
749
            'category' => Category::CATEGORY_DATABASE,
750
            'functionCall' => [Database::class, 'DPRODUCT'],
751
            'argumentCount' => '3',
752
        ],
753
        'DSTDEV' => [
754
            'category' => Category::CATEGORY_DATABASE,
755
            'functionCall' => [Database::class, 'DSTDEV'],
756
            'argumentCount' => '3',
757
        ],
758
        'DSTDEVP' => [
759
            'category' => Category::CATEGORY_DATABASE,
760
            'functionCall' => [Database::class, 'DSTDEVP'],
761
            'argumentCount' => '3',
762
        ],
763
        'DSUM' => [
764
            'category' => Category::CATEGORY_DATABASE,
765
            'functionCall' => [Database::class, 'DSUM'],
766
            'argumentCount' => '3',
767
        ],
768
        'DURATION' => [
769
            'category' => Category::CATEGORY_FINANCIAL,
770
            'functionCall' => [Functions::class, 'DUMMY'],
771
            'argumentCount' => '5,6',
772
        ],
773
        'DVAR' => [
774
            'category' => Category::CATEGORY_DATABASE,
775
            'functionCall' => [Database::class, 'DVAR'],
776
            'argumentCount' => '3',
777
        ],
778
        'DVARP' => [
779
            'category' => Category::CATEGORY_DATABASE,
780
            'functionCall' => [Database::class, 'DVARP'],
781
            'argumentCount' => '3',
782
        ],
783
        'EDATE' => [
784
            'category' => Category::CATEGORY_DATE_AND_TIME,
785
            'functionCall' => [DateTime::class, 'EDATE'],
786
            'argumentCount' => '2',
787
        ],
788
        'EFFECT' => [
789
            'category' => Category::CATEGORY_FINANCIAL,
790
            'functionCall' => [Financial::class, 'EFFECT'],
791
            'argumentCount' => '2',
792
        ],
793
        'EOMONTH' => [
794
            'category' => Category::CATEGORY_DATE_AND_TIME,
795
            'functionCall' => [DateTime::class, 'EOMONTH'],
796
            'argumentCount' => '2',
797
        ],
798
        'ERF' => [
799
            'category' => Category::CATEGORY_ENGINEERING,
800
            'functionCall' => [Engineering::class, 'ERF'],
801
            'argumentCount' => '1,2',
802
        ],
803
        'ERF.PRECISE' => [
804
            'category' => Category::CATEGORY_ENGINEERING,
805
            'functionCall' => [Engineering::class, 'ERFPRECISE'],
806
            'argumentCount' => '1',
807
        ],
808
        'ERFC' => [
809
            'category' => Category::CATEGORY_ENGINEERING,
810
            'functionCall' => [Engineering::class, 'ERFC'],
811
            'argumentCount' => '1',
812
        ],
813
        'ERFC.PRECISE' => [
814
            'category' => Category::CATEGORY_ENGINEERING,
815
            'functionCall' => [Engineering::class, 'ERFC'],
816
            'argumentCount' => '1',
817
        ],
818
        'ERROR.TYPE' => [
819
            'category' => Category::CATEGORY_INFORMATION,
820
            'functionCall' => [Functions::class, 'errorType'],
821
            'argumentCount' => '1',
822
        ],
823
        'EVEN' => [
824
            'category' => Category::CATEGORY_MATH_AND_TRIG,
825
            'functionCall' => [MathTrig::class, 'EVEN'],
826
            'argumentCount' => '1',
827
        ],
828
        'EXACT' => [
829
            'category' => Category::CATEGORY_TEXT_AND_DATA,
830
            'functionCall' => [TextData::class, 'EXACT'],
831
            'argumentCount' => '2',
832
        ],
833
        'EXP' => [
834
            'category' => Category::CATEGORY_MATH_AND_TRIG,
835
            'functionCall' => 'exp',
836
            'argumentCount' => '1',
837
        ],
838
        'EXPONDIST' => [
839
            'category' => Category::CATEGORY_STATISTICAL,
840
            'functionCall' => [Statistical::class, 'EXPONDIST'],
841
            'argumentCount' => '3',
842
        ],
843
        'FACT' => [
844
            'category' => Category::CATEGORY_MATH_AND_TRIG,
845
            'functionCall' => [MathTrig::class, 'FACT'],
846
            'argumentCount' => '1',
847
        ],
848
        'FACTDOUBLE' => [
849
            'category' => Category::CATEGORY_MATH_AND_TRIG,
850
            'functionCall' => [MathTrig::class, 'FACTDOUBLE'],
851
            'argumentCount' => '1',
852
        ],
853
        'FALSE' => [
854
            'category' => Category::CATEGORY_LOGICAL,
855
            'functionCall' => [Logical::class, 'FALSE'],
856
            'argumentCount' => '0',
857
        ],
858
        'FDIST' => [
859
            'category' => Category::CATEGORY_STATISTICAL,
860
            'functionCall' => [Functions::class, 'DUMMY'],
861
            'argumentCount' => '3',
862
        ],
863
        'FIND' => [
864
            'category' => Category::CATEGORY_TEXT_AND_DATA,
865
            'functionCall' => [TextData::class, 'SEARCHSENSITIVE'],
866
            'argumentCount' => '2,3',
867
        ],
868
        'FINDB' => [
869
            'category' => Category::CATEGORY_TEXT_AND_DATA,
870
            'functionCall' => [TextData::class, 'SEARCHSENSITIVE'],
871
            'argumentCount' => '2,3',
872
        ],
873
        'FINV' => [
874
            'category' => Category::CATEGORY_STATISTICAL,
875
            'functionCall' => [Functions::class, 'DUMMY'],
876
            'argumentCount' => '3',
877
        ],
878
        'FISHER' => [
879
            'category' => Category::CATEGORY_STATISTICAL,
880
            'functionCall' => [Statistical::class, 'FISHER'],
881
            'argumentCount' => '1',
882
        ],
883
        'FISHERINV' => [
884
            'category' => Category::CATEGORY_STATISTICAL,
885
            'functionCall' => [Statistical::class, 'FISHERINV'],
886
            'argumentCount' => '1',
887
        ],
888
        'FIXED' => [
889
            'category' => Category::CATEGORY_TEXT_AND_DATA,
890
            'functionCall' => [TextData::class, 'FIXEDFORMAT'],
891
            'argumentCount' => '1-3',
892
        ],
893
        'FLOOR' => [
894
            'category' => Category::CATEGORY_MATH_AND_TRIG,
895
            'functionCall' => [MathTrig::class, 'FLOOR'],
896
            'argumentCount' => '2',
897
        ],
898
        'FORECAST' => [
899
            'category' => Category::CATEGORY_STATISTICAL,
900
            'functionCall' => [Statistical::class, 'FORECAST'],
901
            'argumentCount' => '3',
902
        ],
903
        'FORMULATEXT' => [
904
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
905
            'functionCall' => [LookupRef::class, 'FORMULATEXT'],
906
            'argumentCount' => '1',
907
            'passCellReference' => true,
908
            'passByReference' => [true],
909
        ],
910
        'FREQUENCY' => [
911
            'category' => Category::CATEGORY_STATISTICAL,
912
            'functionCall' => [Functions::class, 'DUMMY'],
913
            'argumentCount' => '2',
914
        ],
915
        'FTEST' => [
916
            'category' => Category::CATEGORY_STATISTICAL,
917
            'functionCall' => [Functions::class, 'DUMMY'],
918
            'argumentCount' => '2',
919
        ],
920
        'FV' => [
921
            'category' => Category::CATEGORY_FINANCIAL,
922
            'functionCall' => [Financial::class, 'FV'],
923
            'argumentCount' => '3-5',
924
        ],
925
        'FVSCHEDULE' => [
926
            'category' => Category::CATEGORY_FINANCIAL,
927
            'functionCall' => [Financial::class, 'FVSCHEDULE'],
928
            'argumentCount' => '2',
929
        ],
930
        'GAMMADIST' => [
931
            'category' => Category::CATEGORY_STATISTICAL,
932
            'functionCall' => [Statistical::class, 'GAMMADIST'],
933
            'argumentCount' => '4',
934
        ],
935
        'GAMMAINV' => [
936
            'category' => Category::CATEGORY_STATISTICAL,
937
            'functionCall' => [Statistical::class, 'GAMMAINV'],
938
            'argumentCount' => '3',
939
        ],
940
        'GAMMALN' => [
941
            'category' => Category::CATEGORY_STATISTICAL,
942
            'functionCall' => [Statistical::class, 'GAMMALN'],
943
            'argumentCount' => '1',
944
        ],
945
        'GCD' => [
946
            'category' => Category::CATEGORY_MATH_AND_TRIG,
947
            'functionCall' => [MathTrig::class, 'GCD'],
948
            'argumentCount' => '1+',
949
        ],
950
        'GEOMEAN' => [
951
            'category' => Category::CATEGORY_STATISTICAL,
952
            'functionCall' => [Statistical::class, 'GEOMEAN'],
953
            'argumentCount' => '1+',
954
        ],
955
        'GESTEP' => [
956
            'category' => Category::CATEGORY_ENGINEERING,
957
            'functionCall' => [Engineering::class, 'GESTEP'],
958
            'argumentCount' => '1,2',
959
        ],
960
        'GETPIVOTDATA' => [
961
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
962
            'functionCall' => [Functions::class, 'DUMMY'],
963
            'argumentCount' => '2+',
964
        ],
965
        'GROWTH' => [
966
            'category' => Category::CATEGORY_STATISTICAL,
967
            'functionCall' => [Statistical::class, 'GROWTH'],
968
            'argumentCount' => '1-4',
969
        ],
970
        'HARMEAN' => [
971
            'category' => Category::CATEGORY_STATISTICAL,
972
            'functionCall' => [Statistical::class, 'HARMEAN'],
973
            'argumentCount' => '1+',
974
        ],
975
        'HEX2BIN' => [
976
            'category' => Category::CATEGORY_ENGINEERING,
977
            'functionCall' => [Engineering::class, 'HEXTOBIN'],
978
            'argumentCount' => '1,2',
979
        ],
980
        'HEX2DEC' => [
981
            'category' => Category::CATEGORY_ENGINEERING,
982
            'functionCall' => [Engineering::class, 'HEXTODEC'],
983
            'argumentCount' => '1',
984
        ],
985
        'HEX2OCT' => [
986
            'category' => Category::CATEGORY_ENGINEERING,
987
            'functionCall' => [Engineering::class, 'HEXTOOCT'],
988
            'argumentCount' => '1,2',
989
        ],
990
        'HLOOKUP' => [
991
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
992
            'functionCall' => [LookupRef::class, 'HLOOKUP'],
993
            'argumentCount' => '3,4',
994
        ],
995
        'HOUR' => [
996
            'category' => Category::CATEGORY_DATE_AND_TIME,
997
            'functionCall' => [DateTime::class, 'HOUROFDAY'],
998
            'argumentCount' => '1',
999
        ],
1000
        'HYPERLINK' => [
1001
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1002
            'functionCall' => [LookupRef::class, 'HYPERLINK'],
1003
            'argumentCount' => '1,2',
1004
            'passCellReference' => true,
1005
        ],
1006
        'HYPGEOMDIST' => [
1007
            'category' => Category::CATEGORY_STATISTICAL,
1008
            'functionCall' => [Statistical::class, 'HYPGEOMDIST'],
1009
            'argumentCount' => '4',
1010
        ],
1011
        'IF' => [
1012
            'category' => Category::CATEGORY_LOGICAL,
1013
            'functionCall' => [Logical::class, 'statementIf'],
1014
            'argumentCount' => '1-3',
1015
        ],
1016
        'IFERROR' => [
1017
            'category' => Category::CATEGORY_LOGICAL,
1018
            'functionCall' => [Logical::class, 'IFERROR'],
1019
            'argumentCount' => '2',
1020
        ],
1021
        'IMABS' => [
1022
            'category' => Category::CATEGORY_ENGINEERING,
1023
            'functionCall' => [Engineering::class, 'IMABS'],
1024
            'argumentCount' => '1',
1025
        ],
1026
        'IMAGINARY' => [
1027
            'category' => Category::CATEGORY_ENGINEERING,
1028
            'functionCall' => [Engineering::class, 'IMAGINARY'],
1029
            'argumentCount' => '1',
1030
        ],
1031
        'IMARGUMENT' => [
1032
            'category' => Category::CATEGORY_ENGINEERING,
1033
            'functionCall' => [Engineering::class, 'IMARGUMENT'],
1034
            'argumentCount' => '1',
1035
        ],
1036
        'IMCONJUGATE' => [
1037
            'category' => Category::CATEGORY_ENGINEERING,
1038
            'functionCall' => [Engineering::class, 'IMCONJUGATE'],
1039
            'argumentCount' => '1',
1040
        ],
1041
        'IMCOS' => [
1042
            'category' => Category::CATEGORY_ENGINEERING,
1043
            'functionCall' => [Engineering::class, 'IMCOS'],
1044
            'argumentCount' => '1',
1045
        ],
1046
        'IMCOSH' => [
1047
            'category' => Category::CATEGORY_ENGINEERING,
1048
            'functionCall' => [Engineering::class, 'IMCOSH'],
1049
            'argumentCount' => '1',
1050
        ],
1051
        'IMCOT' => [
1052
            'category' => Category::CATEGORY_ENGINEERING,
1053
            'functionCall' => [Engineering::class, 'IMCOT'],
1054
            'argumentCount' => '1',
1055
        ],
1056
        'IMCSC' => [
1057
            'category' => Category::CATEGORY_ENGINEERING,
1058
            'functionCall' => [Engineering::class, 'IMCSC'],
1059
            'argumentCount' => '1',
1060
        ],
1061
        'IMCSCH' => [
1062
            'category' => Category::CATEGORY_ENGINEERING,
1063
            'functionCall' => [Engineering::class, 'IMCSCH'],
1064
            'argumentCount' => '1',
1065
        ],
1066
        'IMDIV' => [
1067
            'category' => Category::CATEGORY_ENGINEERING,
1068
            'functionCall' => [Engineering::class, 'IMDIV'],
1069
            'argumentCount' => '2',
1070
        ],
1071
        'IMEXP' => [
1072
            'category' => Category::CATEGORY_ENGINEERING,
1073
            'functionCall' => [Engineering::class, 'IMEXP'],
1074
            'argumentCount' => '1',
1075
        ],
1076
        'IMLN' => [
1077
            'category' => Category::CATEGORY_ENGINEERING,
1078
            'functionCall' => [Engineering::class, 'IMLN'],
1079
            'argumentCount' => '1',
1080
        ],
1081
        'IMLOG10' => [
1082
            'category' => Category::CATEGORY_ENGINEERING,
1083
            'functionCall' => [Engineering::class, 'IMLOG10'],
1084
            'argumentCount' => '1',
1085
        ],
1086
        'IMLOG2' => [
1087
            'category' => Category::CATEGORY_ENGINEERING,
1088
            'functionCall' => [Engineering::class, 'IMLOG2'],
1089
            'argumentCount' => '1',
1090
        ],
1091
        'IMPOWER' => [
1092
            'category' => Category::CATEGORY_ENGINEERING,
1093
            'functionCall' => [Engineering::class, 'IMPOWER'],
1094
            'argumentCount' => '2',
1095
        ],
1096
        'IMPRODUCT' => [
1097
            'category' => Category::CATEGORY_ENGINEERING,
1098
            'functionCall' => [Engineering::class, 'IMPRODUCT'],
1099
            'argumentCount' => '1+',
1100
        ],
1101
        'IMREAL' => [
1102
            'category' => Category::CATEGORY_ENGINEERING,
1103
            'functionCall' => [Engineering::class, 'IMREAL'],
1104
            'argumentCount' => '1',
1105
        ],
1106
        'IMSEC' => [
1107
            'category' => Category::CATEGORY_ENGINEERING,
1108
            'functionCall' => [Engineering::class, 'IMSEC'],
1109
            'argumentCount' => '1',
1110
        ],
1111
        'IMSECH' => [
1112
            'category' => Category::CATEGORY_ENGINEERING,
1113
            'functionCall' => [Engineering::class, 'IMSECH'],
1114
            'argumentCount' => '1',
1115
        ],
1116
        'IMSIN' => [
1117
            'category' => Category::CATEGORY_ENGINEERING,
1118
            'functionCall' => [Engineering::class, 'IMSIN'],
1119
            'argumentCount' => '1',
1120
        ],
1121
        'IMSINH' => [
1122
            'category' => Category::CATEGORY_ENGINEERING,
1123
            'functionCall' => [Engineering::class, 'IMSINH'],
1124
            'argumentCount' => '1',
1125
        ],
1126
        'IMSQRT' => [
1127
            'category' => Category::CATEGORY_ENGINEERING,
1128
            'functionCall' => [Engineering::class, 'IMSQRT'],
1129
            'argumentCount' => '1',
1130
        ],
1131
        'IMSUB' => [
1132
            'category' => Category::CATEGORY_ENGINEERING,
1133
            'functionCall' => [Engineering::class, 'IMSUB'],
1134
            'argumentCount' => '2',
1135
        ],
1136
        'IMSUM' => [
1137
            'category' => Category::CATEGORY_ENGINEERING,
1138
            'functionCall' => [Engineering::class, 'IMSUM'],
1139
            'argumentCount' => '1+',
1140
        ],
1141
        'IMTAN' => [
1142
            'category' => Category::CATEGORY_ENGINEERING,
1143
            'functionCall' => [Engineering::class, 'IMTAN'],
1144
            'argumentCount' => '1',
1145
        ],
1146
        'INDEX' => [
1147
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1148
            'functionCall' => [LookupRef::class, 'INDEX'],
1149
            'argumentCount' => '1-4',
1150
        ],
1151
        'INDIRECT' => [
1152
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1153
            'functionCall' => [LookupRef::class, 'INDIRECT'],
1154
            'argumentCount' => '1,2',
1155
            'passCellReference' => true,
1156
        ],
1157
        'INFO' => [
1158
            'category' => Category::CATEGORY_INFORMATION,
1159
            'functionCall' => [Functions::class, 'DUMMY'],
1160
            'argumentCount' => '1',
1161
        ],
1162
        'INT' => [
1163
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1164
            'functionCall' => [MathTrig::class, 'INT'],
1165
            'argumentCount' => '1',
1166
        ],
1167
        'INTERCEPT' => [
1168
            'category' => Category::CATEGORY_STATISTICAL,
1169
            'functionCall' => [Statistical::class, 'INTERCEPT'],
1170
            'argumentCount' => '2',
1171
        ],
1172
        'INTRATE' => [
1173
            'category' => Category::CATEGORY_FINANCIAL,
1174
            'functionCall' => [Financial::class, 'INTRATE'],
1175
            'argumentCount' => '4,5',
1176
        ],
1177
        'IPMT' => [
1178
            'category' => Category::CATEGORY_FINANCIAL,
1179
            'functionCall' => [Financial::class, 'IPMT'],
1180
            'argumentCount' => '4-6',
1181
        ],
1182
        'IRR' => [
1183
            'category' => Category::CATEGORY_FINANCIAL,
1184
            'functionCall' => [Financial::class, 'IRR'],
1185
            'argumentCount' => '1,2',
1186
        ],
1187
        'ISBLANK' => [
1188
            'category' => Category::CATEGORY_INFORMATION,
1189
            'functionCall' => [Functions::class, 'isBlank'],
1190
            'argumentCount' => '1',
1191
        ],
1192
        'ISERR' => [
1193
            'category' => Category::CATEGORY_INFORMATION,
1194
            'functionCall' => [Functions::class, 'isErr'],
1195
            'argumentCount' => '1',
1196
        ],
1197
        'ISERROR' => [
1198
            'category' => Category::CATEGORY_INFORMATION,
1199
            'functionCall' => [Functions::class, 'isError'],
1200
            'argumentCount' => '1',
1201
        ],
1202
        'ISEVEN' => [
1203
            'category' => Category::CATEGORY_INFORMATION,
1204
            'functionCall' => [Functions::class, 'isEven'],
1205
            'argumentCount' => '1',
1206
        ],
1207
        'ISFORMULA' => [
1208
            'category' => Category::CATEGORY_INFORMATION,
1209
            'functionCall' => [Functions::class, 'isFormula'],
1210
            'argumentCount' => '1',
1211
            'passCellReference' => true,
1212
            'passByReference' => [true],
1213
        ],
1214
        'ISLOGICAL' => [
1215
            'category' => Category::CATEGORY_INFORMATION,
1216
            'functionCall' => [Functions::class, 'isLogical'],
1217
            'argumentCount' => '1',
1218
        ],
1219
        'ISNA' => [
1220
            'category' => Category::CATEGORY_INFORMATION,
1221
            'functionCall' => [Functions::class, 'isNa'],
1222
            'argumentCount' => '1',
1223
        ],
1224
        'ISNONTEXT' => [
1225
            'category' => Category::CATEGORY_INFORMATION,
1226
            'functionCall' => [Functions::class, 'isNonText'],
1227
            'argumentCount' => '1',
1228
        ],
1229
        'ISNUMBER' => [
1230
            'category' => Category::CATEGORY_INFORMATION,
1231
            'functionCall' => [Functions::class, 'isNumber'],
1232
            'argumentCount' => '1',
1233
        ],
1234
        'ISODD' => [
1235
            'category' => Category::CATEGORY_INFORMATION,
1236
            'functionCall' => [Functions::class, 'isOdd'],
1237
            'argumentCount' => '1',
1238
        ],
1239
        'ISOWEEKNUM' => [
1240
            'category' => Category::CATEGORY_DATE_AND_TIME,
1241
            'functionCall' => [DateTime::class, 'ISOWEEKNUM'],
1242
            'argumentCount' => '1',
1243
        ],
1244
        'ISPMT' => [
1245
            'category' => Category::CATEGORY_FINANCIAL,
1246
            'functionCall' => [Financial::class, 'ISPMT'],
1247
            'argumentCount' => '4',
1248
        ],
1249
        'ISREF' => [
1250
            'category' => Category::CATEGORY_INFORMATION,
1251
            'functionCall' => [Functions::class, 'DUMMY'],
1252
            'argumentCount' => '1',
1253
        ],
1254
        'ISTEXT' => [
1255
            'category' => Category::CATEGORY_INFORMATION,
1256
            'functionCall' => [Functions::class, 'isText'],
1257
            'argumentCount' => '1',
1258
        ],
1259
        'JIS' => [
1260
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1261
            'functionCall' => [Functions::class, 'DUMMY'],
1262
            'argumentCount' => '1',
1263
        ],
1264
        'KURT' => [
1265
            'category' => Category::CATEGORY_STATISTICAL,
1266
            'functionCall' => [Statistical::class, 'KURT'],
1267
            'argumentCount' => '1+',
1268
        ],
1269
        'LARGE' => [
1270
            'category' => Category::CATEGORY_STATISTICAL,
1271
            'functionCall' => [Statistical::class, 'LARGE'],
1272
            'argumentCount' => '2',
1273
        ],
1274
        'LCM' => [
1275
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1276
            'functionCall' => [MathTrig::class, 'LCM'],
1277
            'argumentCount' => '1+',
1278
        ],
1279
        'LEFT' => [
1280
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1281
            'functionCall' => [TextData::class, 'LEFT'],
1282
            'argumentCount' => '1,2',
1283
        ],
1284
        'LEFTB' => [
1285
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1286
            'functionCall' => [TextData::class, 'LEFT'],
1287
            'argumentCount' => '1,2',
1288
        ],
1289
        'LEN' => [
1290
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1291
            'functionCall' => [TextData::class, 'STRINGLENGTH'],
1292
            'argumentCount' => '1',
1293
        ],
1294
        'LENB' => [
1295
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1296
            'functionCall' => [TextData::class, 'STRINGLENGTH'],
1297
            'argumentCount' => '1',
1298
        ],
1299
        'LINEST' => [
1300
            'category' => Category::CATEGORY_STATISTICAL,
1301
            'functionCall' => [Statistical::class, 'LINEST'],
1302
            'argumentCount' => '1-4',
1303
        ],
1304
        'LN' => [
1305
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1306
            'functionCall' => 'log',
1307
            'argumentCount' => '1',
1308
        ],
1309
        'LOG' => [
1310
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1311
            'functionCall' => [MathTrig::class, 'logBase'],
1312
            'argumentCount' => '1,2',
1313
        ],
1314
        'LOG10' => [
1315
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1316
            'functionCall' => 'log10',
1317
            'argumentCount' => '1',
1318
        ],
1319
        'LOGEST' => [
1320
            'category' => Category::CATEGORY_STATISTICAL,
1321
            'functionCall' => [Statistical::class, 'LOGEST'],
1322
            'argumentCount' => '1-4',
1323
        ],
1324
        'LOGINV' => [
1325
            'category' => Category::CATEGORY_STATISTICAL,
1326
            'functionCall' => [Statistical::class, 'LOGINV'],
1327
            'argumentCount' => '3',
1328
        ],
1329
        'LOGNORMDIST' => [
1330
            'category' => Category::CATEGORY_STATISTICAL,
1331
            'functionCall' => [Statistical::class, 'LOGNORMDIST'],
1332
            'argumentCount' => '3',
1333
        ],
1334
        'LOOKUP' => [
1335
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1336
            'functionCall' => [LookupRef::class, 'LOOKUP'],
1337
            'argumentCount' => '2,3',
1338
        ],
1339
        'LOWER' => [
1340
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1341
            'functionCall' => [TextData::class, 'LOWERCASE'],
1342
            'argumentCount' => '1',
1343
        ],
1344
        'MATCH' => [
1345
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1346
            'functionCall' => [LookupRef::class, 'MATCH'],
1347
            'argumentCount' => '2,3',
1348
        ],
1349
        'MAX' => [
1350
            'category' => Category::CATEGORY_STATISTICAL,
1351
            'functionCall' => [Statistical::class, 'MAX'],
1352
            'argumentCount' => '1+',
1353
        ],
1354
        'MAXA' => [
1355
            'category' => Category::CATEGORY_STATISTICAL,
1356
            'functionCall' => [Statistical::class, 'MAXA'],
1357
            'argumentCount' => '1+',
1358
        ],
1359
        'MAXIF' => [
1360
            'category' => Category::CATEGORY_STATISTICAL,
1361
            'functionCall' => [Statistical::class, 'MAXIF'],
1362
            'argumentCount' => '2+',
1363
        ],
1364
        'MDETERM' => [
1365
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1366
            'functionCall' => [MathTrig::class, 'MDETERM'],
1367
            'argumentCount' => '1',
1368
        ],
1369
        'MDURATION' => [
1370
            'category' => Category::CATEGORY_FINANCIAL,
1371
            'functionCall' => [Functions::class, 'DUMMY'],
1372
            'argumentCount' => '5,6',
1373
        ],
1374
        'MEDIAN' => [
1375
            'category' => Category::CATEGORY_STATISTICAL,
1376
            'functionCall' => [Statistical::class, 'MEDIAN'],
1377
            'argumentCount' => '1+',
1378
        ],
1379
        'MEDIANIF' => [
1380
            'category' => Category::CATEGORY_STATISTICAL,
1381
            'functionCall' => [Functions::class, 'DUMMY'],
1382
            'argumentCount' => '2+',
1383
        ],
1384
        'MID' => [
1385
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1386
            'functionCall' => [TextData::class, 'MID'],
1387
            'argumentCount' => '3',
1388
        ],
1389
        'MIDB' => [
1390
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1391
            'functionCall' => [TextData::class, 'MID'],
1392
            'argumentCount' => '3',
1393
        ],
1394
        'MIN' => [
1395
            'category' => Category::CATEGORY_STATISTICAL,
1396
            'functionCall' => [Statistical::class, 'MIN'],
1397
            'argumentCount' => '1+',
1398
        ],
1399
        'MINA' => [
1400
            'category' => Category::CATEGORY_STATISTICAL,
1401
            'functionCall' => [Statistical::class, 'MINA'],
1402
            'argumentCount' => '1+',
1403
        ],
1404
        'MINIF' => [
1405
            'category' => Category::CATEGORY_STATISTICAL,
1406
            'functionCall' => [Statistical::class, 'MINIF'],
1407
            'argumentCount' => '2+',
1408
        ],
1409
        'MINUTE' => [
1410
            'category' => Category::CATEGORY_DATE_AND_TIME,
1411
            'functionCall' => [DateTime::class, 'MINUTE'],
1412
            'argumentCount' => '1',
1413
        ],
1414
        'MINVERSE' => [
1415
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1416
            'functionCall' => [MathTrig::class, 'MINVERSE'],
1417
            'argumentCount' => '1',
1418
        ],
1419
        'MIRR' => [
1420
            'category' => Category::CATEGORY_FINANCIAL,
1421
            'functionCall' => [Financial::class, 'MIRR'],
1422
            'argumentCount' => '3',
1423
        ],
1424
        'MMULT' => [
1425
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1426
            'functionCall' => [MathTrig::class, 'MMULT'],
1427
            'argumentCount' => '2',
1428
        ],
1429
        'MOD' => [
1430
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1431
            'functionCall' => [MathTrig::class, 'MOD'],
1432
            'argumentCount' => '2',
1433
        ],
1434
        'MODE' => [
1435
            'category' => Category::CATEGORY_STATISTICAL,
1436
            'functionCall' => [Statistical::class, 'MODE'],
1437
            'argumentCount' => '1+',
1438
        ],
1439
        'MODE.SNGL' => [
1440
            'category' => Category::CATEGORY_STATISTICAL,
1441
            'functionCall' => [Statistical::class, 'MODE'],
1442
            'argumentCount' => '1+',
1443
        ],
1444
        'MONTH' => [
1445
            'category' => Category::CATEGORY_DATE_AND_TIME,
1446
            'functionCall' => [DateTime::class, 'MONTHOFYEAR'],
1447
            'argumentCount' => '1',
1448
        ],
1449
        'MROUND' => [
1450
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1451
            'functionCall' => [MathTrig::class, 'MROUND'],
1452
            'argumentCount' => '2',
1453
        ],
1454
        'MULTINOMIAL' => [
1455
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1456
            'functionCall' => [MathTrig::class, 'MULTINOMIAL'],
1457
            'argumentCount' => '1+',
1458
        ],
1459
        'N' => [
1460
            'category' => Category::CATEGORY_INFORMATION,
1461
            'functionCall' => [Functions::class, 'n'],
1462
            'argumentCount' => '1',
1463
        ],
1464
        'NA' => [
1465
            'category' => Category::CATEGORY_INFORMATION,
1466
            'functionCall' => [Functions::class, 'NA'],
1467
            'argumentCount' => '0',
1468
        ],
1469
        'NEGBINOMDIST' => [
1470
            'category' => Category::CATEGORY_STATISTICAL,
1471
            'functionCall' => [Statistical::class, 'NEGBINOMDIST'],
1472
            'argumentCount' => '3',
1473
        ],
1474
        'NETWORKDAYS' => [
1475
            'category' => Category::CATEGORY_DATE_AND_TIME,
1476
            'functionCall' => [DateTime::class, 'NETWORKDAYS'],
1477
            'argumentCount' => '2+',
1478
        ],
1479
        'NOMINAL' => [
1480
            'category' => Category::CATEGORY_FINANCIAL,
1481
            'functionCall' => [Financial::class, 'NOMINAL'],
1482
            'argumentCount' => '2',
1483
        ],
1484
        'NORMDIST' => [
1485
            'category' => Category::CATEGORY_STATISTICAL,
1486
            'functionCall' => [Statistical::class, 'NORMDIST'],
1487
            'argumentCount' => '4',
1488
        ],
1489
        'NORMINV' => [
1490
            'category' => Category::CATEGORY_STATISTICAL,
1491
            'functionCall' => [Statistical::class, 'NORMINV'],
1492
            'argumentCount' => '3',
1493
        ],
1494
        'NORMSDIST' => [
1495
            'category' => Category::CATEGORY_STATISTICAL,
1496
            'functionCall' => [Statistical::class, 'NORMSDIST'],
1497
            'argumentCount' => '1',
1498
        ],
1499
        'NORMSINV' => [
1500
            'category' => Category::CATEGORY_STATISTICAL,
1501
            'functionCall' => [Statistical::class, 'NORMSINV'],
1502
            'argumentCount' => '1',
1503
        ],
1504
        'NOT' => [
1505
            'category' => Category::CATEGORY_LOGICAL,
1506
            'functionCall' => [Logical::class, 'NOT'],
1507
            'argumentCount' => '1',
1508
        ],
1509
        'NOW' => [
1510
            'category' => Category::CATEGORY_DATE_AND_TIME,
1511
            'functionCall' => [DateTime::class, 'DATETIMENOW'],
1512
            'argumentCount' => '0',
1513
        ],
1514
        'NPER' => [
1515
            'category' => Category::CATEGORY_FINANCIAL,
1516
            'functionCall' => [Financial::class, 'NPER'],
1517
            'argumentCount' => '3-5',
1518
        ],
1519
        'NPV' => [
1520
            'category' => Category::CATEGORY_FINANCIAL,
1521
            'functionCall' => [Financial::class, 'NPV'],
1522
            'argumentCount' => '2+',
1523
        ],
1524
        'NUMBERVALUE' => [
1525
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1526
            'functionCall' => [TextData::class, 'NUMBERVALUE'],
1527
            'argumentCount' => '1+',
1528
        ],
1529
        'OCT2BIN' => [
1530
            'category' => Category::CATEGORY_ENGINEERING,
1531
            'functionCall' => [Engineering::class, 'OCTTOBIN'],
1532
            'argumentCount' => '1,2',
1533
        ],
1534
        'OCT2DEC' => [
1535
            'category' => Category::CATEGORY_ENGINEERING,
1536
            'functionCall' => [Engineering::class, 'OCTTODEC'],
1537
            'argumentCount' => '1',
1538
        ],
1539
        'OCT2HEX' => [
1540
            'category' => Category::CATEGORY_ENGINEERING,
1541
            'functionCall' => [Engineering::class, 'OCTTOHEX'],
1542
            'argumentCount' => '1,2',
1543
        ],
1544
        'ODD' => [
1545
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1546
            'functionCall' => [MathTrig::class, 'ODD'],
1547
            'argumentCount' => '1',
1548
        ],
1549
        'ODDFPRICE' => [
1550
            'category' => Category::CATEGORY_FINANCIAL,
1551
            'functionCall' => [Functions::class, 'DUMMY'],
1552
            'argumentCount' => '8,9',
1553
        ],
1554
        'ODDFYIELD' => [
1555
            'category' => Category::CATEGORY_FINANCIAL,
1556
            'functionCall' => [Functions::class, 'DUMMY'],
1557
            'argumentCount' => '8,9',
1558
        ],
1559
        'ODDLPRICE' => [
1560
            'category' => Category::CATEGORY_FINANCIAL,
1561
            'functionCall' => [Functions::class, 'DUMMY'],
1562
            'argumentCount' => '7,8',
1563
        ],
1564
        'ODDLYIELD' => [
1565
            'category' => Category::CATEGORY_FINANCIAL,
1566
            'functionCall' => [Functions::class, 'DUMMY'],
1567
            'argumentCount' => '7,8',
1568
        ],
1569
        'OFFSET' => [
1570
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1571
            'functionCall' => [LookupRef::class, 'OFFSET'],
1572
            'argumentCount' => '3-5',
1573
            'passCellReference' => true,
1574
            'passByReference' => [true],
1575
        ],
1576
        'OR' => [
1577
            'category' => Category::CATEGORY_LOGICAL,
1578
            'functionCall' => [Logical::class, 'logicalOr'],
1579
            'argumentCount' => '1+',
1580
        ],
1581
        'PDURATION' => [
1582
            'category' => Category::CATEGORY_FINANCIAL,
1583
            'functionCall' => [Financial::class, 'PDURATION'],
1584
            'argumentCount' => '3',
1585
        ],
1586
        'PEARSON' => [
1587
            'category' => Category::CATEGORY_STATISTICAL,
1588
            'functionCall' => [Statistical::class, 'CORREL'],
1589
            'argumentCount' => '2',
1590
        ],
1591
        'PERCENTILE' => [
1592
            'category' => Category::CATEGORY_STATISTICAL,
1593
            'functionCall' => [Statistical::class, 'PERCENTILE'],
1594
            'argumentCount' => '2',
1595
        ],
1596
        'PERCENTRANK' => [
1597
            'category' => Category::CATEGORY_STATISTICAL,
1598
            'functionCall' => [Statistical::class, 'PERCENTRANK'],
1599
            'argumentCount' => '2,3',
1600
        ],
1601
        'PERMUT' => [
1602
            'category' => Category::CATEGORY_STATISTICAL,
1603
            'functionCall' => [Statistical::class, 'PERMUT'],
1604
            'argumentCount' => '2',
1605
        ],
1606
        'PHONETIC' => [
1607
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1608
            'functionCall' => [Functions::class, 'DUMMY'],
1609
            'argumentCount' => '1',
1610
        ],
1611
        'PI' => [
1612
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1613
            'functionCall' => 'pi',
1614
            'argumentCount' => '0',
1615
        ],
1616
        'PMT' => [
1617
            'category' => Category::CATEGORY_FINANCIAL,
1618
            'functionCall' => [Financial::class, 'PMT'],
1619
            'argumentCount' => '3-5',
1620
        ],
1621
        'POISSON' => [
1622
            'category' => Category::CATEGORY_STATISTICAL,
1623
            'functionCall' => [Statistical::class, 'POISSON'],
1624
            'argumentCount' => '3',
1625
        ],
1626
        'POWER' => [
1627
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1628
            'functionCall' => [MathTrig::class, 'POWER'],
1629
            'argumentCount' => '2',
1630
        ],
1631
        'PPMT' => [
1632
            'category' => Category::CATEGORY_FINANCIAL,
1633
            'functionCall' => [Financial::class, 'PPMT'],
1634
            'argumentCount' => '4-6',
1635
        ],
1636
        'PRICE' => [
1637
            'category' => Category::CATEGORY_FINANCIAL,
1638
            'functionCall' => [Financial::class, 'PRICE'],
1639
            'argumentCount' => '6,7',
1640
        ],
1641
        'PRICEDISC' => [
1642
            'category' => Category::CATEGORY_FINANCIAL,
1643
            'functionCall' => [Financial::class, 'PRICEDISC'],
1644
            'argumentCount' => '4,5',
1645
        ],
1646
        'PRICEMAT' => [
1647
            'category' => Category::CATEGORY_FINANCIAL,
1648
            'functionCall' => [Financial::class, 'PRICEMAT'],
1649
            'argumentCount' => '5,6',
1650
        ],
1651
        'PROB' => [
1652
            'category' => Category::CATEGORY_STATISTICAL,
1653
            'functionCall' => [Functions::class, 'DUMMY'],
1654
            'argumentCount' => '3,4',
1655
        ],
1656
        'PRODUCT' => [
1657
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1658
            'functionCall' => [MathTrig::class, 'PRODUCT'],
1659
            'argumentCount' => '1+',
1660
        ],
1661
        'PROPER' => [
1662
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1663
            'functionCall' => [TextData::class, 'PROPERCASE'],
1664
            'argumentCount' => '1',
1665
        ],
1666
        'PV' => [
1667
            'category' => Category::CATEGORY_FINANCIAL,
1668
            'functionCall' => [Financial::class, 'PV'],
1669
            'argumentCount' => '3-5',
1670
        ],
1671
        'QUARTILE' => [
1672
            'category' => Category::CATEGORY_STATISTICAL,
1673
            'functionCall' => [Statistical::class, 'QUARTILE'],
1674
            'argumentCount' => '2',
1675
        ],
1676
        'QUOTIENT' => [
1677
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1678
            'functionCall' => [MathTrig::class, 'QUOTIENT'],
1679
            'argumentCount' => '2',
1680
        ],
1681
        'RADIANS' => [
1682
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1683
            'functionCall' => 'deg2rad',
1684
            'argumentCount' => '1',
1685
        ],
1686
        'RAND' => [
1687
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1688
            'functionCall' => [MathTrig::class, 'RAND'],
1689
            'argumentCount' => '0',
1690
        ],
1691
        'RANDBETWEEN' => [
1692
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1693
            'functionCall' => [MathTrig::class, 'RAND'],
1694
            'argumentCount' => '2',
1695
        ],
1696
        'RANK' => [
1697
            'category' => Category::CATEGORY_STATISTICAL,
1698
            'functionCall' => [Statistical::class, 'RANK'],
1699
            'argumentCount' => '2,3',
1700
        ],
1701
        'RATE' => [
1702
            'category' => Category::CATEGORY_FINANCIAL,
1703
            'functionCall' => [Financial::class, 'RATE'],
1704
            'argumentCount' => '3-6',
1705
        ],
1706
        'RECEIVED' => [
1707
            'category' => Category::CATEGORY_FINANCIAL,
1708
            'functionCall' => [Financial::class, 'RECEIVED'],
1709
            'argumentCount' => '4-5',
1710
        ],
1711
        'REPLACE' => [
1712
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1713
            'functionCall' => [TextData::class, 'REPLACE'],
1714
            'argumentCount' => '4',
1715
        ],
1716
        'REPLACEB' => [
1717
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1718
            'functionCall' => [TextData::class, 'REPLACE'],
1719
            'argumentCount' => '4',
1720
        ],
1721
        'REPT' => [
1722
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1723
            'functionCall' => 'str_repeat',
1724
            'argumentCount' => '2',
1725
        ],
1726
        'RIGHT' => [
1727
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1728
            'functionCall' => [TextData::class, 'RIGHT'],
1729
            'argumentCount' => '1,2',
1730
        ],
1731
        'RIGHTB' => [
1732
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1733
            'functionCall' => [TextData::class, 'RIGHT'],
1734
            'argumentCount' => '1,2',
1735
        ],
1736
        'ROMAN' => [
1737
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1738
            'functionCall' => [MathTrig::class, 'ROMAN'],
1739
            'argumentCount' => '1,2',
1740
        ],
1741
        'ROUND' => [
1742
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1743
            'functionCall' => 'round',
1744
            'argumentCount' => '2',
1745
        ],
1746
        'ROUNDDOWN' => [
1747
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1748
            'functionCall' => [MathTrig::class, 'ROUNDDOWN'],
1749
            'argumentCount' => '2',
1750
        ],
1751
        'ROUNDUP' => [
1752
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1753
            'functionCall' => [MathTrig::class, 'ROUNDUP'],
1754
            'argumentCount' => '2',
1755
        ],
1756
        'ROW' => [
1757
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1758
            'functionCall' => [LookupRef::class, 'ROW'],
1759
            'argumentCount' => '-1',
1760
            'passByReference' => [true],
1761
        ],
1762
        'ROWS' => [
1763
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1764
            'functionCall' => [LookupRef::class, 'ROWS'],
1765
            'argumentCount' => '1',
1766
        ],
1767
        'RRI' => [
1768
            'category' => Category::CATEGORY_FINANCIAL,
1769
            'functionCall' => [Financial::class, 'RRI'],
1770
            'argumentCount' => '3',
1771
        ],
1772
        'RSQ' => [
1773
            'category' => Category::CATEGORY_STATISTICAL,
1774
            'functionCall' => [Statistical::class, 'RSQ'],
1775
            'argumentCount' => '2',
1776
        ],
1777
        'RTD' => [
1778
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1779
            'functionCall' => [Functions::class, 'DUMMY'],
1780
            'argumentCount' => '1+',
1781
        ],
1782
        'SEARCH' => [
1783
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1784
            'functionCall' => [TextData::class, 'SEARCHINSENSITIVE'],
1785
            'argumentCount' => '2,3',
1786
        ],
1787
        'SEARCHB' => [
1788
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1789
            'functionCall' => [TextData::class, 'SEARCHINSENSITIVE'],
1790
            'argumentCount' => '2,3',
1791
        ],
1792
        'SEC' => [
1793
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1794
            'functionCall' => [MathTrig::class, 'SEC'],
1795
            'argumentCount' => '1',
1796
        ],
1797
        'SECH' => [
1798
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1799
            'functionCall' => [MathTrig::class, 'SECH'],
1800
            'argumentCount' => '1',
1801
        ],
1802
        'SECOND' => [
1803
            'category' => Category::CATEGORY_DATE_AND_TIME,
1804
            'functionCall' => [DateTime::class, 'SECOND'],
1805
            'argumentCount' => '1',
1806
        ],
1807
        'SERIESSUM' => [
1808
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1809
            'functionCall' => [MathTrig::class, 'SERIESSUM'],
1810
            'argumentCount' => '4',
1811
        ],
1812
        'SIGN' => [
1813
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1814
            'functionCall' => [MathTrig::class, 'SIGN'],
1815
            'argumentCount' => '1',
1816
        ],
1817
        'SIN' => [
1818
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1819
            'functionCall' => 'sin',
1820
            'argumentCount' => '1',
1821
        ],
1822
        'SINH' => [
1823
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1824
            'functionCall' => 'sinh',
1825
            'argumentCount' => '1',
1826
        ],
1827
        'SKEW' => [
1828
            'category' => Category::CATEGORY_STATISTICAL,
1829
            'functionCall' => [Statistical::class, 'SKEW'],
1830
            'argumentCount' => '1+',
1831
        ],
1832
        'SLN' => [
1833
            'category' => Category::CATEGORY_FINANCIAL,
1834
            'functionCall' => [Financial::class, 'SLN'],
1835
            'argumentCount' => '3',
1836
        ],
1837
        'SLOPE' => [
1838
            'category' => Category::CATEGORY_STATISTICAL,
1839
            'functionCall' => [Statistical::class, 'SLOPE'],
1840
            'argumentCount' => '2',
1841
        ],
1842
        'SMALL' => [
1843
            'category' => Category::CATEGORY_STATISTICAL,
1844
            'functionCall' => [Statistical::class, 'SMALL'],
1845
            'argumentCount' => '2',
1846
        ],
1847
        'SQRT' => [
1848
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1849
            'functionCall' => 'sqrt',
1850
            'argumentCount' => '1',
1851
        ],
1852
        'SQRTPI' => [
1853
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1854
            'functionCall' => [MathTrig::class, 'SQRTPI'],
1855
            'argumentCount' => '1',
1856
        ],
1857
        'STANDARDIZE' => [
1858
            'category' => Category::CATEGORY_STATISTICAL,
1859
            'functionCall' => [Statistical::class, 'STANDARDIZE'],
1860
            'argumentCount' => '3',
1861
        ],
1862
        'STDEV' => [
1863
            'category' => Category::CATEGORY_STATISTICAL,
1864
            'functionCall' => [Statistical::class, 'STDEV'],
1865
            'argumentCount' => '1+',
1866
        ],
1867
        'STDEV.S' => [
1868
            'category' => Category::CATEGORY_STATISTICAL,
1869
            'functionCall' => [Statistical::class, 'STDEV'],
1870
            'argumentCount' => '1+',
1871
        ],
1872
        'STDEV.P' => [
1873
            'category' => Category::CATEGORY_STATISTICAL,
1874
            'functionCall' => [Statistical::class, 'STDEVP'],
1875
            'argumentCount' => '1+',
1876
        ],
1877
        'STDEVA' => [
1878
            'category' => Category::CATEGORY_STATISTICAL,
1879
            'functionCall' => [Statistical::class, 'STDEVA'],
1880
            'argumentCount' => '1+',
1881
        ],
1882
        'STDEVP' => [
1883
            'category' => Category::CATEGORY_STATISTICAL,
1884
            'functionCall' => [Statistical::class, 'STDEVP'],
1885
            'argumentCount' => '1+',
1886
        ],
1887
        'STDEVPA' => [
1888
            'category' => Category::CATEGORY_STATISTICAL,
1889
            'functionCall' => [Statistical::class, 'STDEVPA'],
1890
            'argumentCount' => '1+',
1891
        ],
1892
        'STEYX' => [
1893
            'category' => Category::CATEGORY_STATISTICAL,
1894
            'functionCall' => [Statistical::class, 'STEYX'],
1895
            'argumentCount' => '2',
1896
        ],
1897
        'SUBSTITUTE' => [
1898
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1899
            'functionCall' => [TextData::class, 'SUBSTITUTE'],
1900
            'argumentCount' => '3,4',
1901
        ],
1902
        'SUBTOTAL' => [
1903
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1904
            'functionCall' => [MathTrig::class, 'SUBTOTAL'],
1905
            'argumentCount' => '2+',
1906
            'passCellReference' => true,
1907
        ],
1908
        'SUM' => [
1909
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1910
            'functionCall' => [MathTrig::class, 'SUM'],
1911
            'argumentCount' => '1+',
1912
        ],
1913
        'SUMIF' => [
1914
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1915
            'functionCall' => [MathTrig::class, 'SUMIF'],
1916
            'argumentCount' => '2,3',
1917
        ],
1918
        'SUMIFS' => [
1919
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1920
            'functionCall' => [MathTrig::class, 'SUMIFS'],
1921
            'argumentCount' => '3+',
1922
        ],
1923
        'SUMPRODUCT' => [
1924
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1925
            'functionCall' => [MathTrig::class, 'SUMPRODUCT'],
1926
            'argumentCount' => '1+',
1927
        ],
1928
        'SUMSQ' => [
1929
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1930
            'functionCall' => [MathTrig::class, 'SUMSQ'],
1931
            'argumentCount' => '1+',
1932
        ],
1933
        'SUMX2MY2' => [
1934
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1935
            'functionCall' => [MathTrig::class, 'SUMX2MY2'],
1936
            'argumentCount' => '2',
1937
        ],
1938
        'SUMX2PY2' => [
1939
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1940
            'functionCall' => [MathTrig::class, 'SUMX2PY2'],
1941
            'argumentCount' => '2',
1942
        ],
1943
        'SUMXMY2' => [
1944
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1945
            'functionCall' => [MathTrig::class, 'SUMXMY2'],
1946
            'argumentCount' => '2',
1947
        ],
1948
        'SYD' => [
1949
            'category' => Category::CATEGORY_FINANCIAL,
1950
            'functionCall' => [Financial::class, 'SYD'],
1951
            'argumentCount' => '4',
1952
        ],
1953
        'T' => [
1954
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1955
            'functionCall' => [TextData::class, 'RETURNSTRING'],
1956
            'argumentCount' => '1',
1957
        ],
1958
        'TAN' => [
1959
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1960
            'functionCall' => 'tan',
1961
            'argumentCount' => '1',
1962
        ],
1963
        'TANH' => [
1964
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1965
            'functionCall' => 'tanh',
1966
            'argumentCount' => '1',
1967
        ],
1968
        'TBILLEQ' => [
1969
            'category' => Category::CATEGORY_FINANCIAL,
1970
            'functionCall' => [Financial::class, 'TBILLEQ'],
1971
            'argumentCount' => '3',
1972
        ],
1973
        'TBILLPRICE' => [
1974
            'category' => Category::CATEGORY_FINANCIAL,
1975
            'functionCall' => [Financial::class, 'TBILLPRICE'],
1976
            'argumentCount' => '3',
1977
        ],
1978
        'TBILLYIELD' => [
1979
            'category' => Category::CATEGORY_FINANCIAL,
1980
            'functionCall' => [Financial::class, 'TBILLYIELD'],
1981
            'argumentCount' => '3',
1982
        ],
1983
        'TDIST' => [
1984
            'category' => Category::CATEGORY_STATISTICAL,
1985
            'functionCall' => [Statistical::class, 'TDIST'],
1986
            'argumentCount' => '3',
1987
        ],
1988
        'TEXT' => [
1989
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1990
            'functionCall' => [TextData::class, 'TEXTFORMAT'],
1991
            'argumentCount' => '2',
1992
        ],
1993
        'TEXTJOIN' => [
1994
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1995
            'functionCall' => [TextData::class, 'TEXTJOIN'],
1996
            'argumentCount' => '3+',
1997
        ],
1998
        'TIME' => [
1999
            'category' => Category::CATEGORY_DATE_AND_TIME,
2000
            'functionCall' => [DateTime::class, 'TIME'],
2001
            'argumentCount' => '3',
2002
        ],
2003
        'TIMEVALUE' => [
2004
            'category' => Category::CATEGORY_DATE_AND_TIME,
2005
            'functionCall' => [DateTime::class, 'TIMEVALUE'],
2006
            'argumentCount' => '1',
2007
        ],
2008
        'TINV' => [
2009
            'category' => Category::CATEGORY_STATISTICAL,
2010
            'functionCall' => [Statistical::class, 'TINV'],
2011
            'argumentCount' => '2',
2012
        ],
2013
        'TODAY' => [
2014
            'category' => Category::CATEGORY_DATE_AND_TIME,
2015
            'functionCall' => [DateTime::class, 'DATENOW'],
2016
            'argumentCount' => '0',
2017
        ],
2018
        'TRANSPOSE' => [
2019
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2020
            'functionCall' => [LookupRef::class, 'TRANSPOSE'],
2021
            'argumentCount' => '1',
2022
        ],
2023
        'TREND' => [
2024
            'category' => Category::CATEGORY_STATISTICAL,
2025
            'functionCall' => [Statistical::class, 'TREND'],
2026
            'argumentCount' => '1-4',
2027
        ],
2028
        'TRIM' => [
2029
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2030
            'functionCall' => [TextData::class, 'TRIMSPACES'],
2031
            'argumentCount' => '1',
2032
        ],
2033
        'TRIMMEAN' => [
2034
            'category' => Category::CATEGORY_STATISTICAL,
2035
            'functionCall' => [Statistical::class, 'TRIMMEAN'],
2036
            'argumentCount' => '2',
2037
        ],
2038
        'TRUE' => [
2039
            'category' => Category::CATEGORY_LOGICAL,
2040
            'functionCall' => [Logical::class, 'TRUE'],
2041
            'argumentCount' => '0',
2042
        ],
2043
        'TRUNC' => [
2044
            'category' => Category::CATEGORY_MATH_AND_TRIG,
2045
            'functionCall' => [MathTrig::class, 'TRUNC'],
2046
            'argumentCount' => '1,2',
2047
        ],
2048
        'TTEST' => [
2049
            'category' => Category::CATEGORY_STATISTICAL,
2050
            'functionCall' => [Functions::class, 'DUMMY'],
2051
            'argumentCount' => '4',
2052
        ],
2053
        'TYPE' => [
2054
            'category' => Category::CATEGORY_INFORMATION,
2055
            'functionCall' => [Functions::class, 'TYPE'],
2056
            'argumentCount' => '1',
2057
        ],
2058
        'UNICHAR' => [
2059
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2060
            'functionCall' => [TextData::class, 'CHARACTER'],
2061
            'argumentCount' => '1',
2062
        ],
2063
        'UNICODE' => [
2064
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2065
            'functionCall' => [TextData::class, 'ASCIICODE'],
2066
            'argumentCount' => '1',
2067
        ],
2068
        'UPPER' => [
2069
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2070
            'functionCall' => [TextData::class, 'UPPERCASE'],
2071
            'argumentCount' => '1',
2072
        ],
2073
        'USDOLLAR' => [
2074
            'category' => Category::CATEGORY_FINANCIAL,
2075
            'functionCall' => [Functions::class, 'DUMMY'],
2076
            'argumentCount' => '2',
2077
        ],
2078
        'VALUE' => [
2079
            'category' => Category::CATEGORY_TEXT_AND_DATA,
2080
            'functionCall' => [TextData::class, 'VALUE'],
2081
            'argumentCount' => '1',
2082
        ],
2083
        'VAR' => [
2084
            'category' => Category::CATEGORY_STATISTICAL,
2085
            'functionCall' => [Statistical::class, 'VARFunc'],
2086
            'argumentCount' => '1+',
2087
        ],
2088
        'VAR.P' => [
2089
            'category' => Category::CATEGORY_STATISTICAL,
2090
            'functionCall' => [Statistical::class, 'VARP'],
2091
            'argumentCount' => '1+',
2092
        ],
2093
        'VAR.S' => [
2094
            'category' => Category::CATEGORY_STATISTICAL,
2095
            'functionCall' => [Statistical::class, 'VARFunc'],
2096
            'argumentCount' => '1+',
2097
        ],
2098
        'VARA' => [
2099
            'category' => Category::CATEGORY_STATISTICAL,
2100
            'functionCall' => [Statistical::class, 'VARA'],
2101
            'argumentCount' => '1+',
2102
        ],
2103
        'VARP' => [
2104
            'category' => Category::CATEGORY_STATISTICAL,
2105
            'functionCall' => [Statistical::class, 'VARP'],
2106
            'argumentCount' => '1+',
2107
        ],
2108
        'VARPA' => [
2109
            'category' => Category::CATEGORY_STATISTICAL,
2110
            'functionCall' => [Statistical::class, 'VARPA'],
2111
            'argumentCount' => '1+',
2112
        ],
2113
        'VDB' => [
2114
            'category' => Category::CATEGORY_FINANCIAL,
2115
            'functionCall' => [Functions::class, 'DUMMY'],
2116
            'argumentCount' => '5-7',
2117
        ],
2118
        'VLOOKUP' => [
2119
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2120
            'functionCall' => [LookupRef::class, 'VLOOKUP'],
2121
            'argumentCount' => '3,4',
2122
        ],
2123
        'WEEKDAY' => [
2124
            'category' => Category::CATEGORY_DATE_AND_TIME,
2125
            'functionCall' => [DateTime::class, 'WEEKDAY'],
2126
            'argumentCount' => '1,2',
2127
        ],
2128
        'WEEKNUM' => [
2129
            'category' => Category::CATEGORY_DATE_AND_TIME,
2130
            'functionCall' => [DateTime::class, 'WEEKNUM'],
2131
            'argumentCount' => '1,2',
2132
        ],
2133
        'WEIBULL' => [
2134
            'category' => Category::CATEGORY_STATISTICAL,
2135
            'functionCall' => [Statistical::class, 'WEIBULL'],
2136
            'argumentCount' => '4',
2137
        ],
2138
        'WORKDAY' => [
2139
            'category' => Category::CATEGORY_DATE_AND_TIME,
2140
            'functionCall' => [DateTime::class, 'WORKDAY'],
2141
            'argumentCount' => '2+',
2142
        ],
2143
        'XIRR' => [
2144
            'category' => Category::CATEGORY_FINANCIAL,
2145
            'functionCall' => [Financial::class, 'XIRR'],
2146
            'argumentCount' => '2,3',
2147
        ],
2148
        'XNPV' => [
2149
            'category' => Category::CATEGORY_FINANCIAL,
2150
            'functionCall' => [Financial::class, 'XNPV'],
2151
            'argumentCount' => '3',
2152
        ],
2153
        'XOR' => [
2154
            'category' => Category::CATEGORY_LOGICAL,
2155
            'functionCall' => [Logical::class, 'logicalXor'],
2156
            'argumentCount' => '1+',
2157
        ],
2158
        'YEAR' => [
2159
            'category' => Category::CATEGORY_DATE_AND_TIME,
2160
            'functionCall' => [DateTime::class, 'YEAR'],
2161
            'argumentCount' => '1',
2162
        ],
2163
        'YEARFRAC' => [
2164
            'category' => Category::CATEGORY_DATE_AND_TIME,
2165
            'functionCall' => [DateTime::class, 'YEARFRAC'],
2166
            'argumentCount' => '2,3',
2167
        ],
2168
        'YIELD' => [
2169
            'category' => Category::CATEGORY_FINANCIAL,
2170
            'functionCall' => [Functions::class, 'DUMMY'],
2171
            'argumentCount' => '6,7',
2172
        ],
2173
        'YIELDDISC' => [
2174
            'category' => Category::CATEGORY_FINANCIAL,
2175
            'functionCall' => [Financial::class, 'YIELDDISC'],
2176
            'argumentCount' => '4,5',
2177
        ],
2178
        'YIELDMAT' => [
2179
            'category' => Category::CATEGORY_FINANCIAL,
2180
            'functionCall' => [Financial::class, 'YIELDMAT'],
2181
            'argumentCount' => '5,6',
2182
        ],
2183
        'ZTEST' => [
2184
            'category' => Category::CATEGORY_STATISTICAL,
2185
            'functionCall' => [Statistical::class, 'ZTEST'],
2186
            'argumentCount' => '2-3',
2187
        ],
2188
    ];
2189
2190
    //    Internal functions used for special control purposes
2191
    private static $controlFunctions = [
2192
        'MKMATRIX' => [
2193
            'argumentCount' => '*',
2194
            'functionCall' => 'self::mkMatrix',
2195
        ],
2196
    ];
2197
2198 192
    public function __construct(Spreadsheet $spreadsheet = null)
2199
    {
2200 192
        $this->delta = 1 * pow(10, 0 - ini_get('precision'));
2201
2202 192
        $this->spreadsheet = $spreadsheet;
2203 192
        $this->cyclicReferenceStack = new CyclicReferenceStack();
0 ignored issues
show
Documentation Bug introduced by
It seems like new PhpOffice\PhpSpreads...\CyclicReferenceStack() of type PhpOffice\PhpSpreadsheet...ne\CyclicReferenceStack is incompatible with the declared type array of property $cyclicReferenceStack.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2204 192
        $this->debugLog = new Logger($this->cyclicReferenceStack);
2205 192
    }
2206
2207 1
    private static function loadLocales()
2208
    {
2209 1
        $localeFileDirectory = __DIR__ . '/locale/';
2210 1
        foreach (glob($localeFileDirectory . '/*', GLOB_ONLYDIR) as $filename) {
2211 1
            $filename = substr($filename, strlen($localeFileDirectory) + 1);
2212 1
            if ($filename != 'en') {
2213 1
                self::$validLocaleLanguages[] = $filename;
2214
            }
2215
        }
2216 1
    }
2217
2218
    /**
2219
     * Get an instance of this class.
2220
     *
2221
     * @param Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object,
2222
     *                                    or NULL to create a standalone claculation engine
2223
     *
2224
     * @return Calculation
2225
     */
2226 627
    public static function getInstance(Spreadsheet $spreadsheet = null)
2227
    {
2228 627
        if ($spreadsheet !== null) {
2229 127
            $instance = $spreadsheet->getCalculationEngine();
2230 127
            if (isset($instance)) {
2231 127
                return $instance;
2232
            }
2233
        }
2234
2235 515
        if (!isset(self::$instance) || (self::$instance === null)) {
2236 14
            self::$instance = new self();
2237
        }
2238
2239 515
        return self::$instance;
2240
    }
2241
2242
    /**
2243
     * Flush the calculation cache for any existing instance of this class
2244
     *        but only if a Calculation instance exists.
2245
     */
2246
    public function flushInstance()
2247
    {
2248
        $this->clearCalculationCache();
2249
    }
2250
2251
    /**
2252
     * Get the Logger for this calculation engine instance.
2253
     *
2254
     * @return Logger
2255
     */
2256 98
    public function getDebugLog()
2257
    {
2258 98
        return $this->debugLog;
2259
    }
2260
2261
    /**
2262
     * __clone implementation. Cloning should not be allowed in a Singleton!
2263
     *
2264
     * @throws Exception
2265
     */
2266
    final public function __clone()
2267
    {
2268
        throw new Exception('Cloning the calculation engine is not allowed!');
2269
    }
2270
2271
    /**
2272
     * Return the locale-specific translation of TRUE.
2273
     *
2274
     * @return string locale-specific translation of TRUE
2275
     */
2276 42
    public static function getTRUE()
2277
    {
2278 42
        return self::$localeBoolean['TRUE'];
2279
    }
2280
2281
    /**
2282
     * Return the locale-specific translation of FALSE.
2283
     *
2284
     * @return string locale-specific translation of FALSE
2285
     */
2286 31
    public static function getFALSE()
2287
    {
2288 31
        return self::$localeBoolean['FALSE'];
2289
    }
2290
2291
    /**
2292
     * Set the Array Return Type (Array or Value of first element in the array).
2293
     *
2294
     * @param string $returnType Array return type
2295
     *
2296
     * @return bool Success or failure
2297
     */
2298 26
    public static function setArrayReturnType($returnType)
2299
    {
2300 26
        if (($returnType == self::RETURN_ARRAY_AS_VALUE) ||
2301 20
            ($returnType == self::RETURN_ARRAY_AS_ERROR) ||
2302 26
            ($returnType == self::RETURN_ARRAY_AS_ARRAY)) {
2303 26
            self::$returnArrayAsType = $returnType;
2304
2305 26
            return true;
2306
        }
2307
2308
        return false;
2309
    }
2310
2311
    /**
2312
     * Return the Array Return Type (Array or Value of first element in the array).
2313
     *
2314
     * @return string $returnType Array return type
2315
     */
2316 15
    public static function getArrayReturnType()
2317
    {
2318 15
        return self::$returnArrayAsType;
2319
    }
2320
2321
    /**
2322
     * Is calculation caching enabled?
2323
     *
2324
     * @return bool
2325
     */
2326 1
    public function getCalculationCacheEnabled()
2327
    {
2328 1
        return $this->calculationCacheEnabled;
2329
    }
2330
2331
    /**
2332
     * Enable/disable calculation cache.
2333
     *
2334
     * @param bool $pValue
2335
     */
2336
    public function setCalculationCacheEnabled($pValue)
2337
    {
2338
        $this->calculationCacheEnabled = $pValue;
2339
        $this->clearCalculationCache();
2340
    }
2341
2342
    /**
2343
     * Enable calculation cache.
2344
     */
2345
    public function enableCalculationCache()
2346
    {
2347
        $this->setCalculationCacheEnabled(true);
2348
    }
2349
2350
    /**
2351
     * Disable calculation cache.
2352
     */
2353
    public function disableCalculationCache()
2354
    {
2355
        $this->setCalculationCacheEnabled(false);
2356
    }
2357
2358
    /**
2359
     * Clear calculation cache.
2360
     */
2361
    public function clearCalculationCache()
2362
    {
2363
        $this->calculationCache = [];
2364
    }
2365
2366
    /**
2367
     * Clear calculation cache for a specified worksheet.
2368
     *
2369
     * @param string $worksheetName
2370
     */
2371 26
    public function clearCalculationCacheForWorksheet($worksheetName)
2372
    {
2373 26
        if (isset($this->calculationCache[$worksheetName])) {
2374
            unset($this->calculationCache[$worksheetName]);
2375
        }
2376 26
    }
2377
2378
    /**
2379
     * Rename calculation cache for a specified worksheet.
2380
     *
2381
     * @param string $fromWorksheetName
2382
     * @param string $toWorksheetName
2383
     */
2384 192
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName)
2385
    {
2386 192
        if (isset($this->calculationCache[$fromWorksheetName])) {
2387
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2388
            unset($this->calculationCache[$fromWorksheetName]);
2389
        }
2390 192
    }
2391
2392
    /**
2393
     * Get the currently defined locale code.
2394
     *
2395
     * @return string
2396
     */
2397
    public function getLocale()
2398
    {
2399
        return self::$localeLanguage;
2400
    }
2401
2402
    /**
2403
     * Set the locale code.
2404
     *
2405
     * @param string $locale The locale to use for formula translation, eg: 'en_us'
2406
     *
2407
     * @return bool
2408
     */
2409 468
    public function setLocale($locale)
2410
    {
2411
        //    Identify our locale and language
2412 468
        $language = $locale = strtolower($locale);
2413 468
        if (strpos($locale, '_') !== false) {
2414 468
            list($language) = explode('_', $locale);
2415
        }
2416
2417 468
        if (count(self::$validLocaleLanguages) == 1) {
2418 1
            self::loadLocales();
2419
        }
2420
        //    Test whether we have any language data for this language (any locale)
2421 468
        if (in_array($language, self::$validLocaleLanguages)) {
2422
            //    initialise language/locale settings
2423 468
            self::$localeFunctions = [];
2424 468
            self::$localeArgumentSeparator = ',';
2425 468
            self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL'];
2426
            //    Default is English, if user isn't requesting english, then read the necessary data from the locale files
2427 468
            if ($locale != 'en_us') {
2428
                //    Search for a file with a list of function names for locale
2429 17
                $functionNamesFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'functions';
2430 17
                if (!file_exists($functionNamesFile)) {
2431
                    //    If there isn't a locale specific function file, look for a language specific function file
2432
                    $functionNamesFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'functions';
2433
                    if (!file_exists($functionNamesFile)) {
2434
                        return false;
2435
                    }
2436
                }
2437
                //    Retrieve the list of locale or language specific function names
2438 17
                $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2439 17
                foreach ($localeFunctions as $localeFunction) {
2440 17
                    list($localeFunction) = explode('##', $localeFunction); //    Strip out comments
2441 17
                    if (strpos($localeFunction, '=') !== false) {
2442 17
                        list($fName, $lfName) = explode('=', $localeFunction);
2443 17
                        $fName = trim($fName);
2444 17
                        $lfName = trim($lfName);
2445 17
                        if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) {
2446 17
                            self::$localeFunctions[$fName] = $lfName;
2447
                        }
2448
                    }
2449
                }
2450
                //    Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions
2451 17
                if (isset(self::$localeFunctions['TRUE'])) {
2452 17
                    self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE'];
2453
                }
2454 17
                if (isset(self::$localeFunctions['FALSE'])) {
2455 17
                    self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE'];
2456
                }
2457
2458 17
                $configFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'config';
2459 17
                if (!file_exists($configFile)) {
2460
                    $configFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'config';
2461
                }
2462 17
                if (file_exists($configFile)) {
2463 17
                    $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2464 17
                    foreach ($localeSettings as $localeSetting) {
2465 17
                        list($localeSetting) = explode('##', $localeSetting); //    Strip out comments
2466 17
                        if (strpos($localeSetting, '=') !== false) {
2467 17
                            list($settingName, $settingValue) = explode('=', $localeSetting);
2468 17
                            $settingName = strtoupper(trim($settingName));
2469
                            switch ($settingName) {
2470 17
                                case 'ARGUMENTSEPARATOR':
2471 17
                                    self::$localeArgumentSeparator = trim($settingValue);
2472
2473 17
                                    break;
2474
                            }
2475
                        }
2476
                    }
2477
                }
2478
            }
2479
2480
            self::$functionReplaceFromExcel = self::$functionReplaceToExcel =
2481 468
            self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
2482 468
            self::$localeLanguage = $locale;
2483
2484 468
            return true;
2485
        }
2486
2487
        return false;
2488
    }
2489
2490
    /**
2491
     * @param string $fromSeparator
2492
     * @param string $toSeparator
2493
     * @param string $formula
2494
     * @param bool $inBraces
2495
     *
2496
     * @return string
2497
     */
2498 5
    public static function translateSeparator($fromSeparator, $toSeparator, $formula, &$inBraces)
2499
    {
2500 5
        $strlen = mb_strlen($formula);
2501 5
        for ($i = 0; $i < $strlen; ++$i) {
2502 5
            $chr = mb_substr($formula, $i, 1);
2503
            switch ($chr) {
2504 5
                case '{':
2505
                    $inBraces = true;
2506
2507
                    break;
2508 5
                case '}':
2509
                    $inBraces = false;
2510
2511
                    break;
2512 5
                case $fromSeparator:
2513
                    if (!$inBraces) {
2514
                        $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
2515
                    }
2516
            }
2517
        }
2518
2519 5
        return $formula;
2520
    }
2521
2522
    /**
2523
     * @param string[] $from
2524
     * @param string[] $to
2525
     * @param string $formula
2526
     * @param string $fromSeparator
2527
     * @param string $toSeparator
2528
     *
2529
     * @return string
2530
     */
2531
    private static function translateFormula(array $from, array $to, $formula, $fromSeparator, $toSeparator)
2532
    {
2533
        //    Convert any Excel function names to the required language
2534
        if (self::$localeLanguage !== 'en_us') {
2535
            $inBraces = false;
2536
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
2537
            if (strpos($formula, '"') !== false) {
2538
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
2539
                //        the formula
2540
                $temp = explode('"', $formula);
2541
                $i = false;
2542
                foreach ($temp as &$value) {
2543
                    //    Only count/replace in alternating array entries
2544
                    if ($i = !$i) {
0 ignored issues
show
introduced by
The condition $i is always false.
Loading history...
2545
                        $value = preg_replace($from, $to, $value);
2546
                        $value = self::translateSeparator($fromSeparator, $toSeparator, $value, $inBraces);
2547
                    }
2548
                }
2549
                unset($value);
2550
                //    Then rebuild the formula string
2551
                $formula = implode('"', $temp);
2552
            } else {
2553
                //    If there's no quoted strings, then we do a simple count/replace
2554
                $formula = preg_replace($from, $to, $formula);
2555
                $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inBraces);
2556
            }
2557
        }
2558
2559
        return $formula;
2560
    }
2561
2562
    private static $functionReplaceFromExcel = null;
2563
2564
    private static $functionReplaceToLocale = null;
2565
2566
    public function _translateFormulaToLocale($formula)
2567
    {
2568
        if (self::$functionReplaceFromExcel === null) {
2569
            self::$functionReplaceFromExcel = [];
2570
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2571
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/Ui';
2572
            }
2573
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2574
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui';
2575
            }
2576
        }
2577
2578
        if (self::$functionReplaceToLocale === null) {
2579
            self::$functionReplaceToLocale = [];
2580
            foreach (self::$localeFunctions as $localeFunctionName) {
2581
                self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2';
2582
            }
2583
            foreach (self::$localeBoolean as $localeBoolean) {
2584
                self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2';
2585
            }
2586
        }
2587
2588
        return self::translateFormula(self::$functionReplaceFromExcel, self::$functionReplaceToLocale, $formula, ',', self::$localeArgumentSeparator);
2589
    }
2590
2591
    private static $functionReplaceFromLocale = null;
2592
2593
    private static $functionReplaceToExcel = null;
2594
2595
    public function _translateFormulaToEnglish($formula)
2596
    {
2597
        if (self::$functionReplaceFromLocale === null) {
2598
            self::$functionReplaceFromLocale = [];
2599
            foreach (self::$localeFunctions as $localeFunctionName) {
2600
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/Ui';
2601
            }
2602
            foreach (self::$localeBoolean as $excelBoolean) {
2603
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui';
2604
            }
2605
        }
2606
2607
        if (self::$functionReplaceToExcel === null) {
2608
            self::$functionReplaceToExcel = [];
2609
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2610
                self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2';
2611
            }
2612
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2613
                self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2';
2614
            }
2615
        }
2616
2617
        return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ',');
2618
    }
2619
2620 49
    public static function localeFunc($function)
2621
    {
2622 49
        if (self::$localeLanguage !== 'en_us') {
2623
            $functionName = trim($function, '(');
2624
            if (isset(self::$localeFunctions[$functionName])) {
2625
                $brace = ($functionName != $function);
2626
                $function = self::$localeFunctions[$functionName];
2627
                if ($brace) {
2628
                    $function .= '(';
2629
                }
2630
            }
2631
        }
2632
2633 49
        return $function;
2634
    }
2635
2636
    /**
2637
     * Wrap string values in quotes.
2638
     *
2639
     * @param mixed $value
2640
     *
2641
     * @return mixed
2642
     */
2643 96
    public static function wrapResult($value)
2644
    {
2645 96
        if (is_string($value)) {
2646
            //    Error values cannot be "wrapped"
2647 81
            if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
2648
                //    Return Excel errors "as is"
2649 10
                return $value;
2650
            }
2651
            //    Return strings wrapped in quotes
2652 72
            return '"' . $value . '"';
2653
        //    Convert numeric errors to NaN error
2654 46
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2655
            return Functions::NAN();
2656
        }
2657
2658 46
        return $value;
2659
    }
2660
2661
    /**
2662
     * Remove quotes used as a wrapper to identify string values.
2663
     *
2664
     * @param mixed $value
2665
     *
2666
     * @return mixed
2667
     */
2668 102
    public static function unwrapResult($value)
2669
    {
2670 102
        if (is_string($value)) {
2671 78
            if ((isset($value[0])) && ($value[0] == '"') && (substr($value, -1) == '"')) {
2672 78
                return substr($value, 1, -1);
2673
            }
2674
            //    Convert numeric errors to NAN error
2675 61
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2676
            return Functions::NAN();
2677
        }
2678
2679 62
        return $value;
2680
    }
2681
2682
    /**
2683
     * Calculate cell value (using formula from a cell ID)
2684
     * Retained for backward compatibility.
2685
     *
2686
     * @param Cell $pCell Cell to calculate
2687
     *
2688
     * @throws Exception
2689
     *
2690
     * @return mixed
2691
     */
2692
    public function calculate(Cell $pCell = null)
2693
    {
2694
        try {
2695
            return $this->calculateCellValue($pCell);
2696
        } catch (\Exception $e) {
2697
            throw new Exception($e->getMessage());
2698
        }
2699
    }
2700
2701
    /**
2702
     * Calculate the value of a cell formula.
2703
     *
2704
     * @param Cell $pCell Cell to calculate
2705
     * @param bool $resetLog Flag indicating whether the debug log should be reset or not
2706
     *
2707
     * @throws Exception
2708
     *
2709
     * @return mixed
2710
     */
2711 51
    public function calculateCellValue(Cell $pCell = null, $resetLog = true)
2712
    {
2713 51
        if ($pCell === null) {
2714
            return null;
2715
        }
2716
2717 51
        $returnArrayAsType = self::$returnArrayAsType;
2718 51
        if ($resetLog) {
2719
            //    Initialise the logging settings if requested
2720 50
            $this->formulaError = null;
2721 50
            $this->debugLog->clearLog();
2722 50
            $this->cyclicReferenceStack->clear();
2723 50
            $this->cyclicFormulaCounter = 1;
2724
2725 50
            self::$returnArrayAsType = self::RETURN_ARRAY_AS_ARRAY;
2726
        }
2727
2728
        //    Execute the calculation for the cell formula
2729 51
        $this->cellStack[] = [
2730 51
            'sheet' => $pCell->getWorksheet()->getTitle(),
2731 51
            'cell' => $pCell->getCoordinate(),
2732
        ];
2733
2734
        try {
2735 51
            $result = self::unwrapResult($this->_calculateFormulaValue($pCell->getValue(), $pCell->getCoordinate(), $pCell));
2736 51
            $cellAddress = array_pop($this->cellStack);
2737 51
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2738
        } catch (\Exception $e) {
2739
            $cellAddress = array_pop($this->cellStack);
2740
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2741
2742
            throw new Exception($e->getMessage());
2743
        }
2744
2745 51
        if ((is_array($result)) && (self::$returnArrayAsType != self::RETURN_ARRAY_AS_ARRAY)) {
2746 2
            self::$returnArrayAsType = $returnArrayAsType;
2747 2
            $testResult = Functions::flattenArray($result);
2748 2
            if (self::$returnArrayAsType == self::RETURN_ARRAY_AS_ERROR) {
2749
                return Functions::VALUE();
2750
            }
2751
            //    If there's only a single cell in the array, then we allow it
2752 2
            if (count($testResult) != 1) {
2753
                //    If keys are numeric, then it's a matrix result rather than a cell range result, so we permit it
2754
                $r = array_keys($result);
2755
                $r = array_shift($r);
2756
                if (!is_numeric($r)) {
2757
                    return Functions::VALUE();
2758
                }
2759
                if (is_array($result[$r])) {
2760
                    $c = array_keys($result[$r]);
2761
                    $c = array_shift($c);
2762
                    if (!is_numeric($c)) {
2763
                        return Functions::VALUE();
2764
                    }
2765
                }
2766
            }
2767 2
            $result = array_shift($testResult);
2768
        }
2769 51
        self::$returnArrayAsType = $returnArrayAsType;
2770
2771 51
        if ($result === null) {
2772
            return 0;
2773 51
        } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
2774
            return Functions::NAN();
2775
        }
2776
2777 51
        return $result;
2778
    }
2779
2780
    /**
2781
     * Validate and parse a formula string.
2782
     *
2783
     * @param string $formula Formula to parse
2784
     *
2785
     * @return array|bool
2786
     */
2787 1
    public function parseFormula($formula)
2788
    {
2789
        //    Basic validation that this is indeed a formula
2790
        //    We return an empty array if not
2791 1
        $formula = trim($formula);
2792 1
        if ((!isset($formula[0])) || ($formula[0] != '=')) {
2793
            return [];
2794
        }
2795 1
        $formula = ltrim(substr($formula, 1));
2796 1
        if (!isset($formula[0])) {
2797
            return [];
2798
        }
2799
2800
        //    Parse the formula and return the token stack
2801 1
        return $this->_parseFormula($formula);
2802
    }
2803
2804
    /**
2805
     * Calculate the value of a formula.
2806
     *
2807
     * @param string $formula Formula to parse
2808
     * @param string $cellID Address of the cell to calculate
2809
     * @param Cell $pCell Cell to calculate
2810
     *
2811
     * @throws Exception
2812
     *
2813
     * @return mixed
2814
     */
2815 1
    public function calculateFormula($formula, $cellID = null, Cell $pCell = null)
2816
    {
2817
        //    Initialise the logging settings
2818 1
        $this->formulaError = null;
2819 1
        $this->debugLog->clearLog();
2820 1
        $this->cyclicReferenceStack->clear();
2821
2822 1
        if ($this->spreadsheet !== null && $cellID === null && $pCell === null) {
2823
            $cellID = 'A1';
2824
            $pCell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
2825
        } else {
2826
            //    Disable calculation cacheing because it only applies to cell calculations, not straight formulae
2827
            //    But don't actually flush any cache
2828 1
            $resetCache = $this->getCalculationCacheEnabled();
2829 1
            $this->calculationCacheEnabled = false;
2830
        }
2831
2832
        //    Execute the calculation
2833
        try {
2834 1
            $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $pCell));
2835 1
        } catch (\Exception $e) {
2836 1
            throw new Exception($e->getMessage());
2837
        }
2838
2839 1
        if ($this->spreadsheet === null) {
2840
            //    Reset calculation cacheing to its previous state
2841
            $this->calculationCacheEnabled = $resetCache;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $resetCache does not seem to be defined for all execution paths leading up to this point.
Loading history...
2842
        }
2843
2844 1
        return $result;
2845
    }
2846
2847
    /**
2848
     * @param string $cellReference
2849
     * @param mixed $cellValue
2850
     *
2851
     * @return bool
2852
     */
2853 52
    public function getValueFromCache($cellReference, &$cellValue)
2854
    {
2855
        // Is calculation cacheing enabled?
2856
        // Is the value present in calculation cache?
2857 52
        $this->debugLog->writeDebugLog('Testing cache value for cell ', $cellReference);
2858 52
        if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
2859 42
            $this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache');
2860
            // Return the cached result
2861 42
            $cellValue = $this->calculationCache[$cellReference];
2862
2863 42
            return true;
2864
        }
2865
2866 52
        return false;
2867
    }
2868
2869
    /**
2870
     * @param string $cellReference
2871
     * @param mixed $cellValue
2872
     */
2873 52
    public function saveValueToCache($cellReference, $cellValue)
2874
    {
2875 52
        if ($this->calculationCacheEnabled) {
2876 51
            $this->calculationCache[$cellReference] = $cellValue;
2877
        }
2878 52
    }
2879
2880
    /**
2881
     * Parse a cell formula and calculate its value.
2882
     *
2883
     * @param string $formula The formula to parse and calculate
2884
     * @param string $cellID The ID (e.g. A3) of the cell that we are calculating
2885
     * @param Cell $pCell Cell to calculate
2886
     *
2887
     * @throws Exception
2888
     *
2889
     * @return mixed
2890
     */
2891 126
    public function _calculateFormulaValue($formula, $cellID = null, Cell $pCell = null)
2892
    {
2893 126
        $cellValue = null;
2894
2895
        //    Basic validation that this is indeed a formula
2896
        //    We simply return the cell value if not
2897 126
        $formula = trim($formula);
2898 126
        if ($formula[0] != '=') {
2899
            return self::wrapResult($formula);
2900
        }
2901 126
        $formula = ltrim(substr($formula, 1));
2902 126
        if (!isset($formula[0])) {
2903
            return self::wrapResult($formula);
2904
        }
2905
2906 126
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
2907 126
        $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
2908 126
        $wsCellReference = $wsTitle . '!' . $cellID;
2909
2910 126
        if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
2911 42
            return $cellValue;
2912
        }
2913
2914 126
        if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) {
2915
            if ($this->cyclicFormulaCount <= 0) {
2916
                $this->cyclicFormulaCell = '';
2917
2918
                return $this->raiseFormulaError('Cyclic Reference in Formula');
2919
            } elseif ($this->cyclicFormulaCell === $wsCellReference) {
2920
                ++$this->cyclicFormulaCounter;
2921
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2922
                    $this->cyclicFormulaCell = '';
2923
2924
                    return $cellValue;
2925
                }
2926
            } elseif ($this->cyclicFormulaCell == '') {
2927
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2928
                    return $cellValue;
2929
                }
2930
                $this->cyclicFormulaCell = $wsCellReference;
2931
            }
2932
        }
2933
2934
        //    Parse the formula onto the token stack and calculate the value
2935 126
        $this->cyclicReferenceStack->push($wsCellReference);
2936 126
        $cellValue = $this->processTokenStack($this->_parseFormula($formula, $pCell), $cellID, $pCell);
2937 126
        $this->cyclicReferenceStack->pop();
2938
2939
        // Save to calculation cache
2940 126
        if ($cellID !== null) {
2941 52
            $this->saveValueToCache($wsCellReference, $cellValue);
2942
        }
2943
2944
        //    Return the calculated value
2945 126
        return $cellValue;
2946
    }
2947
2948
    /**
2949
     * Ensure that paired matrix operands are both matrices and of the same size.
2950
     *
2951
     * @param mixed &$operand1 First matrix operand
2952
     * @param mixed &$operand2 Second matrix operand
2953
     * @param int $resize Flag indicating whether the matrices should be resized to match
2954
     *                                        and (if so), whether the smaller dimension should grow or the
2955
     *                                        larger should shrink.
2956
     *                                            0 = no resize
2957
     *                                            1 = shrink to fit
2958
     *                                            2 = extend to fit
2959
     *
2960
     * @return array
2961
     */
2962 5
    private static function checkMatrixOperands(&$operand1, &$operand2, $resize = 1)
2963
    {
2964
        //    Examine each of the two operands, and turn them into an array if they aren't one already
2965
        //    Note that this function should only be called if one or both of the operand is already an array
2966 5
        if (!is_array($operand1)) {
2967
            list($matrixRows, $matrixColumns) = self::getMatrixDimensions($operand2);
2968
            $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1));
2969
            $resize = 0;
2970 5
        } elseif (!is_array($operand2)) {
2971 2
            list($matrixRows, $matrixColumns) = self::getMatrixDimensions($operand1);
2972 2
            $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2));
2973 2
            $resize = 0;
2974
        }
2975
2976 5
        list($matrix1Rows, $matrix1Columns) = self::getMatrixDimensions($operand1);
2977 5
        list($matrix2Rows, $matrix2Columns) = self::getMatrixDimensions($operand2);
2978 5
        if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
2979 5
            $resize = 1;
2980
        }
2981
2982 5
        if ($resize == 2) {
2983
            //    Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger
2984
            self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2985 5
        } elseif ($resize == 1) {
2986
            //    Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller
2987 5
            self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2988
        }
2989
2990 5
        return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns];
2991
    }
2992
2993
    /**
2994
     * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0.
2995
     *
2996
     * @param array &$matrix matrix operand
2997
     *
2998
     * @return int[] An array comprising the number of rows, and number of columns
2999
     */
3000 15
    public static function getMatrixDimensions(array &$matrix)
3001
    {
3002 15
        $matrixRows = count($matrix);
3003 15
        $matrixColumns = 0;
3004 15
        foreach ($matrix as $rowKey => $rowValue) {
3005 13
            if (!is_array($rowValue)) {
3006 4
                $matrix[$rowKey] = [$rowValue];
3007 4
                $matrixColumns = max(1, $matrixColumns);
3008
            } else {
3009 9
                $matrix[$rowKey] = array_values($rowValue);
3010 13
                $matrixColumns = max(count($rowValue), $matrixColumns);
3011
            }
3012
        }
3013 15
        $matrix = array_values($matrix);
3014
3015 15
        return [$matrixRows, $matrixColumns];
3016
    }
3017
3018
    /**
3019
     * Ensure that paired matrix operands are both matrices of the same size.
3020
     *
3021
     * @param mixed &$matrix1 First matrix operand
3022
     * @param mixed &$matrix2 Second matrix operand
3023
     * @param int $matrix1Rows Row size of first matrix operand
3024
     * @param int $matrix1Columns Column size of first matrix operand
3025
     * @param int $matrix2Rows Row size of second matrix operand
3026
     * @param int $matrix2Columns Column size of second matrix operand
3027
     */
3028 5
    private static function resizeMatricesShrink(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
3029
    {
3030 5
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3031
            if ($matrix2Rows < $matrix1Rows) {
3032
                for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) {
3033
                    unset($matrix1[$i]);
3034
                }
3035
            }
3036
            if ($matrix2Columns < $matrix1Columns) {
3037
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3038
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3039
                        unset($matrix1[$i][$j]);
3040
                    }
3041
                }
3042
            }
3043
        }
3044
3045 5
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3046
            if ($matrix1Rows < $matrix2Rows) {
3047
                for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) {
3048
                    unset($matrix2[$i]);
3049
                }
3050
            }
3051
            if ($matrix1Columns < $matrix2Columns) {
3052
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3053
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3054
                        unset($matrix2[$i][$j]);
3055
                    }
3056
                }
3057
            }
3058
        }
3059 5
    }
3060
3061
    /**
3062
     * Ensure that paired matrix operands are both matrices of the same size.
3063
     *
3064
     * @param mixed &$matrix1 First matrix operand
3065
     * @param mixed &$matrix2 Second matrix operand
3066
     * @param int $matrix1Rows Row size of first matrix operand
3067
     * @param int $matrix1Columns Column size of first matrix operand
3068
     * @param int $matrix2Rows Row size of second matrix operand
3069
     * @param int $matrix2Columns Column size of second matrix operand
3070
     */
3071
    private static function resizeMatricesExtend(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
3072
    {
3073
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3074
            if ($matrix2Columns < $matrix1Columns) {
3075
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3076
                    $x = $matrix2[$i][$matrix2Columns - 1];
3077
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3078
                        $matrix2[$i][$j] = $x;
3079
                    }
3080
                }
3081
            }
3082
            if ($matrix2Rows < $matrix1Rows) {
3083
                $x = $matrix2[$matrix2Rows - 1];
3084
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3085
                    $matrix2[$i] = $x;
3086
                }
3087
            }
3088
        }
3089
3090
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3091
            if ($matrix1Columns < $matrix2Columns) {
3092
                for ($i = 0; $i < $matrix1Rows; ++$i) {
3093
                    $x = $matrix1[$i][$matrix1Columns - 1];
3094
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3095
                        $matrix1[$i][$j] = $x;
3096
                    }
3097
                }
3098
            }
3099
            if ($matrix1Rows < $matrix2Rows) {
3100
                $x = $matrix1[$matrix1Rows - 1];
3101
                for ($i = 0; $i < $matrix2Rows; ++$i) {
3102
                    $matrix1[$i] = $x;
3103
                }
3104
            }
3105
        }
3106
    }
3107
3108
    /**
3109
     * Format details of an operand for display in the log (based on operand type).
3110
     *
3111
     * @param mixed $value First matrix operand
3112
     *
3113
     * @return mixed
3114
     */
3115 123
    private function showValue($value)
3116
    {
3117 123
        if ($this->debugLog->getWriteDebugLog()) {
3118
            $testArray = Functions::flattenArray($value);
3119
            if (count($testArray) == 1) {
3120
                $value = array_pop($testArray);
3121
            }
3122
3123
            if (is_array($value)) {
3124
                $returnMatrix = [];
3125
                $pad = $rpad = ', ';
3126
                foreach ($value as $row) {
3127
                    if (is_array($row)) {
3128
                        $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
3129
                        $rpad = '; ';
3130
                    } else {
3131
                        $returnMatrix[] = $this->showValue($row);
3132
                    }
3133
                }
3134
3135
                return '{ ' . implode($rpad, $returnMatrix) . ' }';
3136
            } elseif (is_string($value) && (trim($value, '"') == $value)) {
3137
                return '"' . $value . '"';
3138
            } elseif (is_bool($value)) {
3139
                return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3140
            }
3141
        }
3142
3143 123
        return Functions::flattenSingleValue($value);
3144
    }
3145
3146
    /**
3147
     * Format type and details of an operand for display in the log (based on operand type).
3148
     *
3149
     * @param mixed $value First matrix operand
3150
     *
3151
     * @return null|string
3152
     */
3153 124
    private function showTypeDetails($value)
3154
    {
3155 124
        if ($this->debugLog->getWriteDebugLog()) {
3156
            $testArray = Functions::flattenArray($value);
3157
            if (count($testArray) == 1) {
3158
                $value = array_pop($testArray);
3159
            }
3160
3161
            if ($value === null) {
3162
                return 'a NULL value';
3163
            } elseif (is_float($value)) {
3164
                $typeString = 'a floating point number';
3165
            } elseif (is_int($value)) {
3166
                $typeString = 'an integer number';
3167
            } elseif (is_bool($value)) {
3168
                $typeString = 'a boolean';
3169
            } elseif (is_array($value)) {
3170
                $typeString = 'a matrix';
3171
            } else {
3172
                if ($value == '') {
3173
                    return 'an empty string';
3174
                } elseif ($value[0] == '#') {
3175
                    return 'a ' . $value . ' error';
3176
                }
3177
                $typeString = 'a string';
3178
            }
3179
3180
            return $typeString . ' with a value of ' . $this->showValue($value);
3181
        }
3182 124
    }
3183
3184
    /**
3185
     * @param string $formula
3186
     *
3187
     * @return string
3188
     */
3189 127
    private function convertMatrixReferences($formula)
3190
    {
3191 127
        static $matrixReplaceFrom = ['{', ';', '}'];
3192 127
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
3193
3194
        //    Convert any Excel matrix references to the MKMATRIX() function
3195 127
        if (strpos($formula, '{') !== false) {
3196
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
3197
            if (strpos($formula, '"') !== false) {
3198
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
3199
                //        the formula
3200
                $temp = explode('"', $formula);
3201
                //    Open and Closed counts used for trapping mismatched braces in the formula
3202
                $openCount = $closeCount = 0;
3203
                $i = false;
3204
                foreach ($temp as &$value) {
3205
                    //    Only count/replace in alternating array entries
3206
                    if ($i = !$i) {
0 ignored issues
show
introduced by
The condition $i is always false.
Loading history...
3207
                        $openCount += substr_count($value, '{');
3208
                        $closeCount += substr_count($value, '}');
3209
                        $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
3210
                    }
3211
                }
3212
                unset($value);
3213
                //    Then rebuild the formula string
3214
                $formula = implode('"', $temp);
3215
            } else {
3216
                //    If there's no quoted strings, then we do a simple count/replace
3217
                $openCount = substr_count($formula, '{');
3218
                $closeCount = substr_count($formula, '}');
3219
                $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
3220
            }
3221
            //    Trap for mismatched braces and trigger an appropriate error
3222
            if ($openCount < $closeCount) {
3223
                if ($openCount > 0) {
3224
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->raiseFormu...hed matrix braces '}'') returns the type false which is incompatible with the documented return type string.
Loading history...
3225
                }
3226
3227
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->raiseFormu...ected '}' encountered') returns the type false which is incompatible with the documented return type string.
Loading history...
3228
            } elseif ($openCount > $closeCount) {
3229
                if ($closeCount > 0) {
3230
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->raiseFormu...hed matrix braces '{'') returns the type false which is incompatible with the documented return type string.
Loading history...
3231
                }
3232
3233
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->raiseFormu...ected '{' encountered') returns the type false which is incompatible with the documented return type string.
Loading history...
3234
            }
3235
        }
3236
3237 127
        return $formula;
3238
    }
3239
3240
    private static function mkMatrix(...$args)
3241
    {
3242
        return $args;
3243
    }
3244
3245
    //    Binary Operators
3246
    //    These operators always work on two values
3247
    //    Array key is the operator, the value indicates whether this is a left or right associative operator
3248
    private static $operatorAssociativity = [
3249
        '^' => 0, //    Exponentiation
3250
        '*' => 0, '/' => 0, //    Multiplication and Division
3251
        '+' => 0, '-' => 0, //    Addition and Subtraction
3252
        '&' => 0, //    Concatenation
3253
        '|' => 0, ':' => 0, //    Intersect and Range
3254
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3255
    ];
3256
3257
    //    Comparison (Boolean) Operators
3258
    //    These operators work on two values, but always return a boolean result
3259
    private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
3260
3261
    //    Operator Precedence
3262
    //    This list includes all valid operators, whether binary (including boolean) or unary (such as %)
3263
    //    Array key is the operator, the value is its precedence
3264
    private static $operatorPrecedence = [
3265
        ':' => 8, //    Range
3266
        '|' => 7, //    Intersect
3267
        '~' => 6, //    Negation
3268
        '%' => 5, //    Percentage
3269
        '^' => 4, //    Exponentiation
3270
        '*' => 3, '/' => 3, //    Multiplication and Division
3271
        '+' => 2, '-' => 2, //    Addition and Subtraction
3272
        '&' => 1, //    Concatenation
3273
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3274
    ];
3275
3276
    // Convert infix to postfix notation
3277
3278
    /**
3279
     * @param string $formula
3280
     * @param null|\PhpOffice\PhpSpreadsheet\Cell\Cell $pCell
3281
     *
3282
     * @return bool
3283
     */
3284 127
    private function _parseFormula($formula, Cell $pCell = null)
3285
    {
3286 127
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
0 ignored issues
show
introduced by
The condition $formula = $this->conver...im($formula)) === false is always false.
Loading history...
3287
            return false;
3288
        }
3289
3290
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
3291
        //        so we store the parent worksheet so that we can re-attach it when necessary
3292 127
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3293
3294 127
        $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION .
3295 127
                                '|' . self::CALCULATION_REGEXP_CELLREF .
3296 127
                                '|' . self::CALCULATION_REGEXP_NUMBER .
3297 127
                                '|' . self::CALCULATION_REGEXP_STRING .
3298 127
                                '|' . self::CALCULATION_REGEXP_OPENBRACE .
3299 127
                                '|' . self::CALCULATION_REGEXP_NAMEDRANGE .
3300 127
                                '|' . self::CALCULATION_REGEXP_ERROR .
3301 127
                                ')/si';
3302
3303
        //    Start with initialisation
3304 127
        $index = 0;
3305 127
        $stack = new Stack();
3306 127
        $output = [];
3307 127
        $expectingOperator = false; //    We use this test in syntax-checking the expression to determine when a
3308
                                                    //        - is a negation or + is a positive operator rather than an operation
3309 127
        $expectingOperand = false; //    We use this test in syntax-checking the expression to determine whether an operand
3310
                                                    //        should be null in a function call
3311
        //    The guts of the lexical parser
3312
        //    Loop through the formula extracting each operator and operand in turn
3313 127
        while (true) {
3314 127
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
3315 127
            if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
3316 34
                $opCharacter .= $formula[++$index];
3317
            }
3318
3319
            //    Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
3320 127
            $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
3321
3322 127
            if ($opCharacter == '-' && !$expectingOperator) {                //    Is it a negation instead of a minus?
3323 3
                $stack->push('Unary Operator', '~'); //    Put a negation on the stack
3324 3
                ++$index; //        and drop the negation symbol
3325 127
            } elseif ($opCharacter == '%' && $expectingOperator) {
3326
                $stack->push('Unary Operator', '%'); //    Put a percentage on the stack
3327
                ++$index;
3328 127
            } elseif ($opCharacter == '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
3329
                ++$index; //    Drop the redundant plus symbol
3330 127
            } elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) {    //    We have to explicitly deny a tilde or pipe, because they are legal
3331
                return $this->raiseFormulaError("Formula Error: Illegal character '~'"); //        on the stack but not in the input expression
3332 127
            } elseif ((isset(self::$operators[$opCharacter]) or $isOperandOrFunction) && $expectingOperator) {    //    Are we putting an operator on the stack?
3333 117
                while ($stack->count() > 0 &&
3334 117
                    ($o2 = $stack->last()) &&
3335 117
                    isset(self::$operators[$o2['value']]) &&
3336 117
                    @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3337 2
                    $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3338
                }
3339 117
                $stack->push('Binary Operator', $opCharacter); //    Finally put our current operator onto the stack
3340 117
                ++$index;
3341 117
                $expectingOperator = false;
3342 127
            } elseif ($opCharacter == ')' && $expectingOperator) {            //    Are we expecting to close a parenthesis?
3343 49
                $expectingOperand = false;
3344 49
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3345 40
                    if ($o2 === null) {
3346
                        return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"');
3347
                    }
3348 40
                    $output[] = $o2;
3349
                }
3350 49
                $d = $stack->last(2);
3351 49
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {    //    Did this parenthesis just close a function?
3352 49
                    $functionName = $matches[1]; //    Get the function name
3353 49
                    $d = $stack->pop();
3354 49
                    $argumentCount = $d['value']; //    See how many arguments there were (argument count is the next value stored on the stack)
3355 49
                    $output[] = $d; //    Dump the argument count on the output
3356 49
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
3357 49
                    if (isset(self::$controlFunctions[$functionName])) {
3358
                        $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
3359
                        $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...
3360 49
                    } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3361 49
                        $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
3362 49
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3363
                    } else {    // did we somehow push a non-function on the stack? this should never happen
3364
                        return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack');
3365
                    }
3366
                    //    Check the argument count
3367 49
                    $argumentCountError = false;
3368 49
                    if (is_numeric($expectedArgumentCount)) {
3369 28
                        if ($expectedArgumentCount < 0) {
3370
                            if ($argumentCount > abs($expectedArgumentCount)) {
3371
                                $argumentCountError = true;
3372
                                $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
3373
                            }
3374
                        } else {
3375 28
                            if ($argumentCount != $expectedArgumentCount) {
3376
                                $argumentCountError = true;
3377 28
                                $expectedArgumentCountString = $expectedArgumentCount;
3378
                            }
3379
                        }
3380 41
                    } elseif ($expectedArgumentCount != '*') {
3381 41
                        $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...
3382 41
                        switch ($argMatch[2]) {
3383 41
                            case '+':
3384 38
                                if ($argumentCount < $argMatch[1]) {
3385
                                    $argumentCountError = true;
3386
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
3387
                                }
3388
3389 38
                                break;
3390 16
                            case '-':
3391 13
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
3392
                                    $argumentCountError = true;
3393
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
3394
                                }
3395
3396 13
                                break;
3397 3
                            case ',':
3398 3
                                if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
3399
                                    $argumentCountError = true;
3400
                                    $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
3401
                                }
3402
3403 3
                                break;
3404
                        }
3405
                    }
3406 49
                    if ($argumentCountError) {
3407
                        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...
3408
                    }
3409
                }
3410 49
                ++$index;
3411 127
            } elseif ($opCharacter == ',') {            //    Is this the separator for function arguments?
3412 30
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3413 25
                    if ($o2 === null) {
3414
                        return $this->raiseFormulaError('Formula Error: Unexpected ,');
3415
                    }
3416 25
                    $output[] = $o2; // pop the argument expression stuff and push onto the output
3417
                }
3418
                //    If we've a comma when we're expecting an operand, then what we actually have is a null operand;
3419
                //        so push a null onto the stack
3420 30
                if (($expectingOperand) || (!$expectingOperator)) {
3421
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3422
                }
3423
                // make sure there was a function
3424 30
                $d = $stack->last(2);
3425 30
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {
3426
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
3427
                }
3428 30
                $d = $stack->pop();
3429 30
                $stack->push($d['type'], ++$d['value'], $d['reference']); // increment the argument count
3430 30
                $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again
3431 30
                $expectingOperator = false;
3432 30
                $expectingOperand = true;
3433 30
                ++$index;
3434 127
            } elseif ($opCharacter == '(' && !$expectingOperator) {
3435 3
                $stack->push('Brace', '(');
3436 3
                ++$index;
3437 127
            } elseif ($isOperandOrFunction && !$expectingOperator) {    // do we now have a function/variable/number?
3438 127
                $expectingOperator = true;
3439 127
                $expectingOperand = false;
3440 127
                $val = $match[1];
3441 127
                $length = strlen($val);
3442
3443 127
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
3444 49
                    $val = preg_replace('/\s/u', '', $val);
3445 49
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
3446 49
                        $stack->push('Function', strtoupper($val));
3447 49
                        $ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
3448 49
                        if ($ax) {
3449 10
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 0);
3450 10
                            $expectingOperator = true;
3451
                        } else {
3452 48
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 1);
3453 48
                            $expectingOperator = false;
3454
                        }
3455 49
                        $stack->push('Brace', '(');
3456
                    } else {    // it's a var w/ implicit multiplication
3457 49
                        $output[] = ['type' => 'Value', 'value' => $matches[1], 'reference' => null];
3458
                    }
3459 126
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
3460
                    //    Watch for this case-change when modifying to allow cell references in different worksheets...
3461
                    //    Should only be applied to the actual cell column, not the worksheet name
3462
3463
                    //    If the last entry on the stack was a : operator, then we have a cell range reference
3464 62
                    $testPrevOp = $stack->last(1);
3465 62
                    if ($testPrevOp['value'] == ':') {
3466
                        //    If we have a worksheet reference, then we're playing with a 3D reference
3467 52
                        if ($matches[2] == '') {
3468
                            //    Otherwise, we 'inherit' the worksheet reference from the start cell reference
3469
                            //    The start of the cell range reference should be the last entry in $output
3470 52
                            $startCellRef = $output[count($output) - 1]['value'];
3471 52
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $startCellRef, $startMatches);
3472 52
                            if ($startMatches[2] > '') {
3473 52
                                $val = $startMatches[2] . '!' . $val;
3474
                            }
3475
                        } else {
3476
                            return $this->raiseFormulaError('3D Range references are not yet supported');
3477
                        }
3478
                    }
3479
3480 62
                    $output[] = ['type' => 'Cell Reference', 'value' => $val, 'reference' => $val];
3481
                } else {    // it's a variable, constant, string, number or boolean
3482
                    //    If the last entry on the stack was a : operator, then we may have a row or column range reference
3483 102
                    $testPrevOp = $stack->last(1);
3484 102
                    if ($testPrevOp['value'] == ':') {
3485 1
                        $startRowColRef = $output[count($output) - 1]['value'];
3486 1
                        list($rangeWS1, $startRowColRef) = Worksheet::extractSheetTitle($startRowColRef, true);
3487 1
                        if ($rangeWS1 != '') {
3488
                            $rangeWS1 .= '!';
3489
                        }
3490 1
                        list($rangeWS2, $val) = Worksheet::extractSheetTitle($val, true);
3491 1
                        if ($rangeWS2 != '') {
3492
                            $rangeWS2 .= '!';
3493
                        } else {
3494 1
                            $rangeWS2 = $rangeWS1;
3495
                        }
3496 1
                        if ((is_int($startRowColRef)) && (ctype_digit($val)) &&
3497 1
                            ($startRowColRef <= 1048576) && ($val <= 1048576)) {
3498
                            //    Row range
3499
                            $endRowColRef = ($pCellParent !== null) ? $pCellParent->getHighestColumn() : 'XFD'; //    Max 16,384 columns for Excel2007
3500
                            $output[count($output) - 1]['value'] = $rangeWS1 . 'A' . $startRowColRef;
3501
                            $val = $rangeWS2 . $endRowColRef . $val;
3502 1
                        } elseif ((ctype_alpha($startRowColRef)) && (ctype_alpha($val)) &&
3503 1
                            (strlen($startRowColRef) <= 3) && (strlen($val) <= 3)) {
3504
                            //    Column range
3505
                            $endRowColRef = ($pCellParent !== null) ? $pCellParent->getHighestRow() : 1048576; //    Max 1,048,576 rows for Excel2007
3506
                            $output[count($output) - 1]['value'] = $rangeWS1 . strtoupper($startRowColRef) . '1';
3507
                            $val = $rangeWS2 . $val . $endRowColRef;
3508
                        }
3509
                    }
3510
3511 102
                    $localeConstant = false;
3512 102
                    if ($opCharacter == '"') {
3513
                        //    UnEscape any quotes within the string
3514 64
                        $val = self::wrapResult(str_replace('""', '"', self::unwrapResult($val)));
3515 66
                    } elseif (is_numeric($val)) {
3516 64
                        if ((strpos($val, '.') !== false) || (stripos($val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
3517 20
                            $val = (float) $val;
3518
                        } else {
3519 64
                            $val = (int) $val;
3520
                        }
3521 6
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
3522 1
                        $excelConstant = trim(strtoupper($val));
3523 1
                        $val = self::$excelConstants[$excelConstant];
3524 5
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
3525
                        $val = self::$excelConstants[$localeConstant];
3526
                    }
3527 102
                    $details = ['type' => 'Value', 'value' => $val, 'reference' => null];
3528 102
                    if ($localeConstant) {
3529
                        $details['localeValue'] = $localeConstant;
3530
                    }
3531 102
                    $output[] = $details;
3532
                }
3533 127
                $index += $length;
3534
            } elseif ($opCharacter == '$') {    // absolute row or column range
3535
                ++$index;
3536
            } elseif ($opCharacter == ')') {    // miscellaneous error checking
3537
                if ($expectingOperand) {
3538
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3539
                    $expectingOperand = false;
3540
                    $expectingOperator = true;
3541
                } else {
3542
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
3543
                }
3544
            } elseif (isset(self::$operators[$opCharacter]) && !$expectingOperator) {
3545
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
3546
            } else {    // I don't even want to know what you did to get here
3547
                return $this->raiseFormulaError('Formula Error: An unexpected error occured');
3548
            }
3549
            //    Test for end of formula string
3550 127
            if ($index == strlen($formula)) {
3551
                //    Did we end with an operator?.
3552
                //    Only valid for the % unary operator
3553 127
                if ((isset(self::$operators[$opCharacter])) && ($opCharacter != '%')) {
3554
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
3555
                }
3556
3557 127
                break;
3558
            }
3559
            //    Ignore white space
3560 125
            while (($formula[$index] == "\n") || ($formula[$index] == "\r")) {
3561
                ++$index;
3562
            }
3563 125
            if ($formula[$index] == ' ') {
3564 56
                while ($formula[$index] == ' ') {
3565 56
                    ++$index;
3566
                }
3567
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
3568
                //        Cell References) then we have an INTERSECTION operator
3569 56
                if (($expectingOperator) && (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
3570 56
                    ($output[count($output) - 1]['type'] == 'Cell Reference')) {
3571
                    while ($stack->count() > 0 &&
3572
                        ($o2 = $stack->last()) &&
3573
                        isset(self::$operators[$o2['value']]) &&
3574
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3575
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3576
                    }
3577
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
3578
                    $expectingOperator = false;
3579
                }
3580
            }
3581
        }
3582
3583 127
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
3584 103
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
3585
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
3586
            }
3587 103
            $output[] = $op;
3588
        }
3589
3590 127
        return $output;
3591
    }
3592
3593 116
    private static function dataTestReference(&$operandData)
3594
    {
3595 116
        $operand = $operandData['value'];
3596 116
        if (($operandData['reference'] === null) && (is_array($operand))) {
3597 1
            $rKeys = array_keys($operand);
3598 1
            $rowKey = array_shift($rKeys);
3599 1
            $cKeys = array_keys(array_keys($operand[$rowKey]));
3600 1
            $colKey = array_shift($cKeys);
3601 1
            if (ctype_upper($colKey)) {
3602
                $operandData['reference'] = $colKey . $rowKey;
3603
            }
3604
        }
3605
3606 116
        return $operand;
3607
    }
3608
3609
    // evaluate postfix notation
3610
3611
    /**
3612
     * @param mixed $tokens
3613
     * @param null|string $cellID
3614
     * @param null|Cell $pCell
3615
     *
3616
     * @return bool
3617
     */
3618 126
    private function processTokenStack($tokens, $cellID = null, Cell $pCell = null)
3619
    {
3620 126
        if ($tokens == false) {
3621
            return false;
3622
        }
3623
3624
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
3625
        //        so we store the parent cell collection so that we can re-attach it when necessary
3626 126
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
3627 126
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
3628 126
        $stack = new Stack();
3629
3630
        //    Loop through each token in turn
3631 126
        foreach ($tokens as $tokenData) {
3632 126
            $token = $tokenData['value'];
3633
            // 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
3634 126
            if (isset(self::$binaryOperators[$token])) {
3635
                //    We must have two operands, error if we don't
3636 116
                if (($operand2Data = $stack->pop()) === null) {
3637
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3638
                }
3639 116
                if (($operand1Data = $stack->pop()) === null) {
3640
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3641
                }
3642
3643 116
                $operand1 = self::dataTestReference($operand1Data);
3644 116
                $operand2 = self::dataTestReference($operand2Data);
3645
3646
                //    Log what we're doing
3647 116
                if ($token == ':') {
3648 51
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
3649
                } else {
3650 99
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
3651
                }
3652
3653
                //    Process the operation in the appropriate manner
3654 116
                switch ($token) {
3655
                    //    Comparison (Boolean) Operators
3656 116
                    case '>':            //    Greater than
3657 102
                    case '<':            //    Less than
3658 95
                    case '>=':            //    Greater than or Equal to
3659 87
                    case '<=':            //    Less than or Equal to
3660 80
                    case '=':            //    Equality
3661 67
                    case '<>':            //    Inequality
3662 78
                        $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
3663
3664 78
                        break;
3665
                    //    Binary Operators
3666 59
                    case ':':            //    Range
3667 51
                        if (strpos($operand1Data['reference'], '!') !== false) {
3668 51
                            list($sheet1, $operand1Data['reference']) = Worksheet::extractSheetTitle($operand1Data['reference'], true);
3669
                        } else {
3670 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

3670
                            $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...
3671
                        }
3672
3673 51
                        list($sheet2, $operand2Data['reference']) = Worksheet::extractSheetTitle($operand2Data['reference'], true);
3674 51
                        if (empty($sheet2)) {
3675 10
                            $sheet2 = $sheet1;
3676
                        }
3677
3678 51
                        if ($sheet1 == $sheet2) {
3679 51
                            if ($operand1Data['reference'] === null) {
3680
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
3681
                                    $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

3681
                                    $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...
3682
                                } elseif (trim($operand1Data['reference']) == '') {
3683
                                    $operand1Data['reference'] = $pCell->getCoordinate();
3684
                                } else {
3685
                                    $operand1Data['reference'] = $operand1Data['value'] . $pCell->getRow();
3686
                                }
3687
                            }
3688 51
                            if ($operand2Data['reference'] === null) {
3689
                                if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
3690
                                    $operand2Data['reference'] = $pCell->getColumn() . $operand2Data['value'];
3691
                                } elseif (trim($operand2Data['reference']) == '') {
3692
                                    $operand2Data['reference'] = $pCell->getCoordinate();
3693
                                } else {
3694
                                    $operand2Data['reference'] = $operand2Data['value'] . $pCell->getRow();
3695
                                }
3696
                            }
3697
3698 51
                            $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
3699 51
                            $oCol = $oRow = [];
3700 51
                            foreach ($oData as $oDatum) {
3701 51
                                $oCR = Coordinate::coordinateFromString($oDatum);
3702 51
                                $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
3703 51
                                $oRow[] = $oCR[1];
3704
                            }
3705 51
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
3706 51
                            if ($pCellParent !== null) {
3707 51
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
3708
                            } else {
3709
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3710
                            }
3711 51
                            $stack->push('Cell Reference', $cellValue, $cellRef);
3712
                        } else {
3713
                            $stack->push('Error', Functions::REF(), null);
3714
                        }
3715
3716 51
                        break;
3717 32
                    case '+':            //    Addition
3718 23
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
3719
3720 23
                        break;
3721 27
                    case '-':            //    Subtraction
3722 7
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
3723
3724 7
                        break;
3725 25
                    case '*':            //    Multiplication
3726 18
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
3727
3728 18
                        break;
3729 11
                    case '/':            //    Division
3730 8
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
3731
3732 8
                        break;
3733 6
                    case '^':            //    Exponential
3734
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
3735
3736
                        break;
3737 6
                    case '&':            //    Concatenation
3738
                        //    If either of the operands is a matrix, we need to treat them both as matrices
3739
                        //        (converting the other operand to a matrix if need be); then perform the required
3740
                        //        matrix operation
3741 6
                        if (is_bool($operand1)) {
3742
                            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3743
                        }
3744 6
                        if (is_bool($operand2)) {
3745
                            $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3746
                        }
3747 6
                        if ((is_array($operand1)) || (is_array($operand2))) {
3748
                            //    Ensure that both operands are arrays/matrices
3749 5
                            self::checkMatrixOperands($operand1, $operand2, 2);
3750
3751
                            try {
3752
                                //    Convert operand 1 from a PHP array to a matrix
3753 5
                                $matrix = new Shared\JAMA\Matrix($operand1);
3754
                                //    Perform the required operation against the operand 1 matrix, passing in operand 2
3755 5
                                $matrixResult = $matrix->concat($operand2);
3756 5
                                $result = $matrixResult->getArray();
3757
                            } catch (\Exception $ex) {
3758
                                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3759 5
                                $result = '#VALUE!';
3760
                            }
3761
                        } else {
3762 1
                            $result = '"' . str_replace('""', '"', self::unwrapResult($operand1) . self::unwrapResult($operand2)) . '"';
3763
                        }
3764 6
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3765 6
                        $stack->push('Value', $result);
3766
3767 6
                        break;
3768
                    case '|':            //    Intersect
3769
                        $rowIntersect = array_intersect_key($operand1, $operand2);
3770
                        $cellIntersect = $oCol = $oRow = [];
3771
                        foreach (array_keys($rowIntersect) as $row) {
3772
                            $oRow[] = $row;
3773
                            foreach ($rowIntersect[$row] as $col => $data) {
3774
                                $oCol[] = Coordinate::columnIndexFromString($col) - 1;
3775
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
3776
                            }
3777
                        }
3778
                        $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
3779
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
3780
                        $stack->push('Value', $cellIntersect, $cellRef);
3781
3782 116
                        break;
3783
                }
3784
3785
                // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
3786 126
            } elseif (($token === '~') || ($token === '%')) {
3787 3
                if (($arg = $stack->pop()) === null) {
3788
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3789
                }
3790 3
                $arg = $arg['value'];
3791 3
                if ($token === '~') {
3792 3
                    $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg));
3793 3
                    $multiplier = -1;
3794
                } else {
3795
                    $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg));
3796
                    $multiplier = 0.01;
3797
                }
3798 3
                if (is_array($arg)) {
3799
                    self::checkMatrixOperands($arg, $multiplier, 2);
3800
3801
                    try {
3802
                        $matrix1 = new Shared\JAMA\Matrix($arg);
3803
                        $matrixResult = $matrix1->arrayTimesEquals($multiplier);
3804
                        $result = $matrixResult->getArray();
3805
                    } catch (\Exception $ex) {
3806
                        $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3807
                        $result = '#VALUE!';
3808
                    }
3809
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3810
                    $stack->push('Value', $result);
3811
                } else {
3812 3
                    $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack);
3813
                }
3814 126
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token, $matches)) {
3815 61
                $cellRef = null;
3816 61
                if (isset($matches[8])) {
3817
                    if ($pCell === null) {
3818
                        //                        We can't access the range, so return a REF error
3819
                        $cellValue = Functions::REF();
3820
                    } else {
3821
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
3822
                        if ($matches[2] > '') {
3823
                            $matches[2] = trim($matches[2], "\"'");
3824
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
3825
                                //    It's a Reference to an external spreadsheet (not currently supported)
3826
                                return $this->raiseFormulaError('Unable to access External Workbook');
3827
                            }
3828
                            $matches[2] = trim($matches[2], "\"'");
3829
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]);
3830
                            if ($pCellParent !== null) {
3831
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3832
                            } else {
3833
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3834
                            }
3835
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3836
                        } else {
3837
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet');
3838
                            if ($pCellParent !== null) {
3839
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
3840
                            } else {
3841
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3842
                            }
3843
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
3844
                        }
3845
                    }
3846
                } else {
3847 61
                    if ($pCell === null) {
3848
                        //                        We can't access the cell, so return a REF error
3849
                        $cellValue = Functions::REF();
3850
                    } else {
3851 61
                        $cellRef = $matches[6] . $matches[7];
3852 61
                        if ($matches[2] > '') {
3853 14
                            $matches[2] = trim($matches[2], "\"'");
3854 14
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
3855
                                //    It's a Reference to an external spreadsheet (not currently supported)
3856
                                return $this->raiseFormulaError('Unable to access External Workbook');
3857
                            }
3858 14
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]);
3859 14
                            if ($pCellParent !== null) {
3860 14
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
3861 14
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
3862 14
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3863 14
                                    $pCell->attach($pCellParent);
3864
                                } else {
3865 14
                                    $cellValue = null;
3866
                                }
3867
                            } else {
3868
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3869
                            }
3870 14
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3871
                        } else {
3872 49
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet');
3873 49
                            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

3873
                            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...
3874 48
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
3875 48
                                $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

3875
                                $pCell->attach(/** @scrutinizer ignore-type */ $pCellParent);
Loading history...
3876
                            } else {
3877 3
                                $cellValue = null;
3878
                            }
3879 49
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
3880
                        }
3881
                    }
3882
                }
3883 61
                $stack->push('Value', $cellValue, $cellRef);
3884
3885
            // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
3886 112
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) {
3887 48
                $functionName = $matches[1];
3888 48
                $argCount = $stack->pop();
3889 48
                $argCount = $argCount['value'];
3890 48
                if ($functionName != 'MKMATRIX') {
3891 48
                    $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's'));
3892
                }
3893 48
                if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) {    // function
3894 48
                    if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3895 48
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3896 48
                        $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
3897 48
                        $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
3898
                    } elseif (isset(self::$controlFunctions[$functionName])) {
3899
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
3900
                        $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
3901
                        $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
3902
                    }
3903
                    // get the arguments for this function
3904 48
                    $args = $argArrayVals = [];
3905 48
                    for ($i = 0; $i < $argCount; ++$i) {
3906 47
                        $arg = $stack->pop();
3907 47
                        $a = $argCount - $i - 1;
3908 47
                        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...
3909 47
                            (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) &&
3910 47
                            (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) {
3911 2
                            if ($arg['reference'] === null) {
3912
                                $args[] = $cellID;
3913
                                if ($functionName != 'MKMATRIX') {
3914
                                    $argArrayVals[] = $this->showValue($cellID);
3915
                                }
3916
                            } else {
3917 2
                                $args[] = $arg['reference'];
3918 2
                                if ($functionName != 'MKMATRIX') {
3919 2
                                    $argArrayVals[] = $this->showValue($arg['reference']);
3920
                                }
3921
                            }
3922
                        } else {
3923 46
                            $args[] = self::unwrapResult($arg['value']);
3924 46
                            if ($functionName != 'MKMATRIX') {
3925 46
                                $argArrayVals[] = $this->showValue($arg['value']);
3926
                            }
3927
                        }
3928
                    }
3929
                    //    Reverse the order of the arguments
3930 48
                    krsort($args);
3931
3932 48
                    if (($passByReference) && ($argCount == 0)) {
3933
                        $args[] = $cellID;
3934
                        $argArrayVals[] = $this->showValue($cellID);
3935
                    }
3936
3937 48
                    if ($functionName != 'MKMATRIX') {
3938 48
                        if ($this->debugLog->getWriteDebugLog()) {
3939
                            krsort($argArrayVals);
3940
                            $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )');
3941
                        }
3942
                    }
3943
3944
                    //    Process the argument with the appropriate function call
3945 48
                    $args = $this->addCellReference($args, $passCellReference, $functionCall, $pCell);
0 ignored issues
show
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...
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...
3946
3947 48
                    if (!is_array($functionCall)) {
3948 1
                        foreach ($args as &$arg) {
3949
                            $arg = Functions::flattenSingleValue($arg);
3950
                        }
3951 1
                        unset($arg);
3952
                    }
3953 48
                    $result = call_user_func_array($functionCall, $args);
3954
3955 48
                    if ($functionName != 'MKMATRIX') {
3956 48
                        $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
3957
                    }
3958 48
                    $stack->push('Value', self::wrapResult($result));
3959
                }
3960
            } else {
3961
                // if the token is a number, boolean, string or an Excel error, push it onto the stack
3962 112
                if (isset(self::$excelConstants[strtoupper($token)])) {
3963
                    $excelConstant = strtoupper($token);
3964
                    $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
3965
                    $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant]));
3966 112
                } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == '"') || ($token[0] == '#')) {
3967 112
                    $stack->push('Value', $token);
3968
                // if the token is a named range, push the named range name onto the stack
3969 5
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) {
3970 5
                    $namedRange = $matches[6];
3971 5
                    $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
3972
3973 5
                    $cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
3974 5
                    $pCell->attach($pCellParent);
3975 5
                    $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
3976 5
                    $stack->push('Named Range', $cellValue, $namedRange);
3977
                } else {
3978 126
                    return $this->raiseFormulaError("undefined variable '$token'");
3979
                }
3980
            }
3981
        }
3982
        // when we're out of tokens, the stack should have a single element, the final result
3983 126
        if ($stack->count() != 1) {
3984
            return $this->raiseFormulaError('internal error');
3985
        }
3986 126
        $output = $stack->pop();
3987 126
        $output = $output['value'];
3988
3989 126
        return $output;
3990
    }
3991
3992 32
    private function validateBinaryOperand(&$operand, &$stack)
3993
    {
3994 32
        if (is_array($operand)) {
3995 21
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
3996
                do {
3997 21
                    $operand = array_pop($operand);
3998 21
                } while (is_array($operand));
3999
            }
4000
        }
4001
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
4002 32
        if (is_string($operand)) {
4003
            //    We only need special validations for the operand if it is a string
4004
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
4005 2
            if ($operand > '' && $operand[0] == '"') {
4006
                $operand = self::unwrapResult($operand);
4007
            }
4008
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
4009 2
            if (!is_numeric($operand)) {
4010
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
4011 2
                if ($operand > '' && $operand[0] == '#') {
4012
                    $stack->push('Value', $operand);
4013
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand));
4014
4015
                    return false;
4016 2
                } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
4017
                    //    If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
4018 2
                    $stack->push('Value', '#VALUE!');
4019 2
                    $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!'));
4020
4021 2
                    return false;
4022
                }
4023
            }
4024
        }
4025
4026
        //    return a true if the value of the operand is one that we can use in normal binary operations
4027 31
        return true;
4028
    }
4029
4030
    /**
4031
     * @param null|string $cellID
4032
     * @param mixed $operand1
4033
     * @param mixed $operand2
4034
     * @param string $operation
4035
     * @param Stack $stack
4036
     * @param bool $recursingArrays
4037
     *
4038
     * @return bool
4039
     */
4040 78
    private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
4041
    {
4042
        //    If we're dealing with matrix operations, we want a matrix result
4043 78
        if ((is_array($operand1)) || (is_array($operand2))) {
4044 11
            $result = [];
4045 11
            if ((is_array($operand1)) && (!is_array($operand2))) {
4046 11
                foreach ($operand1 as $x => $operandData) {
4047 11
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2));
4048 11
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack);
4049 11
                    $r = $stack->pop();
4050 11
                    $result[$x] = $r['value'];
4051
                }
4052
            } elseif ((!is_array($operand1)) && (is_array($operand2))) {
4053
                foreach ($operand2 as $x => $operandData) {
4054
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData));
4055
                    $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack);
4056
                    $r = $stack->pop();
4057
                    $result[$x] = $r['value'];
4058
                }
4059
            } else {
4060
                if (!$recursingArrays) {
4061
                    self::checkMatrixOperands($operand1, $operand2, 2);
4062
                }
4063
                foreach ($operand1 as $x => $operandData) {
4064
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x]));
4065
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true);
4066
                    $r = $stack->pop();
4067
                    $result[$x] = $r['value'];
4068
                }
4069
            }
4070
            //    Log the result details
4071 11
            $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result));
4072
            //    And push the result onto the stack
4073 11
            $stack->push('Array', $result);
4074
4075 11
            return true;
4076
        }
4077
4078
        //    Simple validate the two operands if they are string values
4079 78
        if (is_string($operand1) && $operand1 > '' && $operand1[0] == '"') {
4080 47
            $operand1 = self::unwrapResult($operand1);
4081
        }
4082 78
        if (is_string($operand2) && $operand2 > '' && $operand2[0] == '"') {
4083 57
            $operand2 = self::unwrapResult($operand2);
4084
        }
4085
4086
        // Use case insensitive comparaison if not OpenOffice mode
4087 78
        if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
4088 78
            if (is_string($operand1)) {
4089 47
                $operand1 = strtoupper($operand1);
4090
            }
4091 78
            if (is_string($operand2)) {
4092 57
                $operand2 = strtoupper($operand2);
4093
            }
4094
        }
4095
4096 78
        $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE;
4097
4098
        //    execute the necessary operation
4099 78
        switch ($operation) {
4100
            //    Greater than
4101 78
            case '>':
4102 18
                if ($useLowercaseFirstComparison) {
4103 9
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0;
4104
                } else {
4105 18
                    $result = ($operand1 > $operand2);
4106
                }
4107
4108 18
                break;
4109
            //    Less than
4110 64
            case '<':
4111 8
                if ($useLowercaseFirstComparison) {
4112 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0;
4113
                } else {
4114 8
                    $result = ($operand1 < $operand2);
4115
                }
4116
4117 8
                break;
4118
            //    Equality
4119 57
            case '=':
4120 23
                if (is_numeric($operand1) && is_numeric($operand2)) {
4121 7
                    $result = (abs($operand1 - $operand2) < $this->delta);
4122
                } else {
4123 20
                    $result = strcmp($operand1, $operand2) == 0;
4124
                }
4125
4126 23
                break;
4127
            //    Greater than or equal
4128 34
            case '>=':
4129 8
                if (is_numeric($operand1) && is_numeric($operand2)) {
4130 4
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2));
4131 4
                } elseif ($useLowercaseFirstComparison) {
4132 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0;
4133
                } else {
4134 4
                    $result = strcmp($operand1, $operand2) >= 0;
4135
                }
4136
4137 8
                break;
4138
            //    Less than or equal
4139 26
            case '<=':
4140 7
                if (is_numeric($operand1) && is_numeric($operand2)) {
4141 3
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2));
4142 4
                } elseif ($useLowercaseFirstComparison) {
4143 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0;
4144
                } else {
4145 4
                    $result = strcmp($operand1, $operand2) <= 0;
4146
                }
4147
4148 7
                break;
4149
            //    Inequality
4150 19
            case '<>':
4151 19
                if (is_numeric($operand1) && is_numeric($operand2)) {
4152 3
                    $result = (abs($operand1 - $operand2) > 1E-14);
4153
                } else {
4154 16
                    $result = strcmp($operand1, $operand2) != 0;
4155
                }
4156
4157 19
                break;
4158
        }
4159
4160
        //    Log the result details
4161 78
        $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...
4162
        //    And push the result onto the stack
4163 78
        $stack->push('Value', $result);
4164
4165 78
        return true;
4166
    }
4167
4168
    /**
4169
     * Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
4170
     *
4171
     * @param string $str1 First string value for the comparison
4172
     * @param string $str2 Second string value for the comparison
4173
     *
4174
     * @return int
4175
     */
4176 21
    private function strcmpLowercaseFirst($str1, $str2)
4177
    {
4178 21
        $inversedStr1 = Shared\StringHelper::strCaseReverse($str1);
4179 21
        $inversedStr2 = Shared\StringHelper::strCaseReverse($str2);
4180
4181 21
        return strcmp($inversedStr1, $inversedStr2);
4182
    }
4183
4184
    /**
4185
     * @param mixed $operand1
4186
     * @param mixed $operand2
4187
     * @param mixed $operation
4188
     * @param string $matrixFunction
4189
     * @param mixed $stack
4190
     *
4191
     * @return bool
4192
     */
4193 32
    private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack)
4194
    {
4195
        //    Validate the two operands
4196 32
        if (!$this->validateBinaryOperand($operand1, $stack)) {
4197 2
            return false;
4198
        }
4199 31
        if (!$this->validateBinaryOperand($operand2, $stack)) {
4200 1
            return false;
4201
        }
4202
4203
        //    If either of the operands is a matrix, we need to treat them both as matrices
4204
        //        (converting the other operand to a matrix if need be); then perform the required
4205
        //        matrix operation
4206 31
        if ((is_array($operand1)) || (is_array($operand2))) {
4207
            //    Ensure that both operands are arrays/matrices of the same size
4208
            self::checkMatrixOperands($operand1, $operand2, 2);
4209
4210
            try {
4211
                //    Convert operand 1 from a PHP array to a matrix
4212
                $matrix = new Shared\JAMA\Matrix($operand1);
4213
                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4214
                $matrixResult = $matrix->$matrixFunction($operand2);
4215
                $result = $matrixResult->getArray();
4216
            } catch (\Exception $ex) {
4217
                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4218
                $result = '#VALUE!';
4219
            }
4220
        } else {
4221 31
            if ((Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) &&
4222 31
                ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) ||
4223 31
                 (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0))) {
4224
                $result = Functions::VALUE();
4225
            } else {
4226
                //    If we're dealing with non-matrix operations, execute the necessary operation
4227 31
                switch ($operation) {
4228
                    //    Addition
4229 31
                    case '+':
4230 22
                        $result = $operand1 + $operand2;
4231
4232 22
                        break;
4233
                    //    Subtraction
4234 26
                    case '-':
4235 7
                        $result = $operand1 - $operand2;
4236
4237 7
                        break;
4238
                    //    Multiplication
4239 24
                    case '*':
4240 19
                        $result = $operand1 * $operand2;
4241
4242 19
                        break;
4243
                    //    Division
4244 8
                    case '/':
4245 8
                        if ($operand2 == 0) {
4246
                            //    Trap for Divide by Zero error
4247 8
                            $stack->push('Value', '#DIV/0!');
4248 8
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails('#DIV/0!'));
4249
4250 8
                            return false;
4251
                        }
4252
                            $result = $operand1 / $operand2;
4253
4254
                        break;
4255
                    //    Power
4256
                    case '^':
4257
                        $result = pow($operand1, $operand2);
4258
4259
                        break;
4260
                }
4261
            }
4262
        }
4263
4264
        //    Log the result details
4265 26
        $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...
4266
        //    And push the result onto the stack
4267 26
        $stack->push('Value', $result);
4268
4269 26
        return true;
4270
    }
4271
4272
    // trigger an error, but nicely, if need be
4273
    protected function raiseFormulaError($errorMessage)
4274
    {
4275
        $this->formulaError = $errorMessage;
4276
        $this->cyclicReferenceStack->clear();
4277
        if (!$this->suppressFormulaErrors) {
4278
            throw new Exception($errorMessage);
4279
        }
4280
        trigger_error($errorMessage, E_USER_ERROR);
4281
4282
        return false;
4283
    }
4284
4285
    /**
4286
     * Extract range values.
4287
     *
4288
     * @param string &$pRange String based range representation
4289
     * @param Worksheet $pSheet Worksheet
4290
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4291
     *
4292
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4293
     */
4294 61
    public function extractCellRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4295
    {
4296
        // Return value
4297 61
        $returnValue = [];
4298
4299 61
        if ($pSheet !== null) {
4300 61
            $pSheetName = $pSheet->getTitle();
4301 61
            if (strpos($pRange, '!') !== false) {
4302
                list($pSheetName, $pRange) = Worksheet::extractSheetTitle($pRange, true);
4303
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4304
            }
4305
4306
            // Extract range
4307 61
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4308 61
            $pRange = $pSheetName . '!' . $pRange;
4309 61
            if (!isset($aReferences[1])) {
4310 61
                $currentCol = '';
4311 61
                $currentRow = 0;
4312
                //    Single cell in range
4313 61
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
4314 61
                if ($pSheet->cellExists($aReferences[0])) {
4315 61
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4316
                } else {
4317 61
                    $returnValue[$currentRow][$currentCol] = null;
4318
                }
4319
            } else {
4320
                // Extract cell data for all cells in the range
4321 51
                foreach ($aReferences as $reference) {
4322 51
                    $currentCol = '';
4323 51
                    $currentRow = 0;
4324
                    // Extract range
4325 51
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
4326 51
                    if ($pSheet->cellExists($reference)) {
4327 51
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4328
                    } else {
4329 51
                        $returnValue[$currentRow][$currentCol] = null;
4330
                    }
4331
                }
4332
            }
4333
        }
4334
4335 61
        return $returnValue;
4336
    }
4337
4338
    /**
4339
     * Extract range values.
4340
     *
4341
     * @param string &$pRange String based range representation
4342
     * @param Worksheet $pSheet Worksheet
4343
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4344
     *
4345
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4346
     */
4347 5
    public function extractNamedRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4348
    {
4349
        // Return value
4350 5
        $returnValue = [];
4351
4352 5
        if ($pSheet !== null) {
4353 5
            $pSheetName = $pSheet->getTitle();
0 ignored issues
show
Unused Code introduced by
The assignment to $pSheetName is dead and can be removed.
Loading history...
4354 5
            if (strpos($pRange, '!') !== false) {
4355
                list($pSheetName, $pRange) = Worksheet::extractSheetTitle($pRange, true);
4356
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4357
            }
4358
4359
            // Named range?
4360 5
            $namedRange = NamedRange::resolveRange($pRange, $pSheet);
4361 5
            if ($namedRange !== null) {
4362 2
                $pSheet = $namedRange->getWorksheet();
4363 2
                $pRange = $namedRange->getRange();
4364 2
                $splitRange = Coordinate::splitRange($pRange);
4365
                //    Convert row and column references
4366 2
                if (ctype_alpha($splitRange[0][0])) {
4367
                    $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
4368 2
                } elseif (ctype_digit($splitRange[0][0])) {
4369 2
                    $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
4370
                }
4371
            } else {
4372 3
                return Functions::REF();
4373
            }
4374
4375
            // Extract range
4376 2
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4377 2
            if (!isset($aReferences[1])) {
4378
                //    Single cell (or single column or row) in range
4379 1
                list($currentCol, $currentRow) = Coordinate::coordinateFromString($aReferences[0]);
4380 1
                if ($pSheet->cellExists($aReferences[0])) {
4381 1
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4382
                } else {
4383 1
                    $returnValue[$currentRow][$currentCol] = null;
4384
                }
4385
            } else {
4386
                // Extract cell data for all cells in the range
4387 1
                foreach ($aReferences as $reference) {
4388
                    // Extract range
4389 1
                    list($currentCol, $currentRow) = Coordinate::coordinateFromString($reference);
4390 1
                    if ($pSheet->cellExists($reference)) {
4391 1
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4392
                    } else {
4393 1
                        $returnValue[$currentRow][$currentCol] = null;
4394
                    }
4395
                }
4396
            }
4397
        }
4398
4399 2
        return $returnValue;
4400
    }
4401
4402
    /**
4403
     * Is a specific function implemented?
4404
     *
4405
     * @param string $pFunction Function Name
4406
     *
4407
     * @return bool
4408
     */
4409 3
    public function isImplemented($pFunction)
4410
    {
4411 3
        $pFunction = strtoupper($pFunction);
4412 3
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
4413
4414 3
        return !$notImplemented;
4415
    }
4416
4417
    /**
4418
     * Get a list of all implemented functions as an array of function objects.
4419
     *
4420
     * @return array of Category
4421
     */
4422
    public function getFunctions()
4423
    {
4424
        return self::$phpSpreadsheetFunctions;
4425
    }
4426
4427
    /**
4428
     * Get a list of implemented Excel function names.
4429
     *
4430
     * @return array
4431
     */
4432 2
    public function getImplementedFunctionNames()
4433
    {
4434 2
        $returnValue = [];
4435 2
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
4436 2
            if ($this->isImplemented($functionName)) {
4437 2
                $returnValue[] = $functionName;
4438
            }
4439
        }
4440
4441 2
        return $returnValue;
4442
    }
4443
4444
    /**
4445
     * Add cell reference if needed while making sure that it is the last argument.
4446
     *
4447
     * @param array $args
4448
     * @param bool $passCellReference
4449
     * @param array|string $functionCall
4450
     * @param null|Cell $pCell
4451
     *
4452
     * @return array
4453
     */
4454 48
    private function addCellReference(array $args, $passCellReference, $functionCall, Cell $pCell = null)
4455
    {
4456 48
        if ($passCellReference) {
4457 3
            if (is_array($functionCall)) {
4458 3
                $className = $functionCall[0];
4459 3
                $methodName = $functionCall[1];
4460
4461 3
                $reflectionMethod = new \ReflectionMethod($className, $methodName);
4462 3
                $argumentCount = count($reflectionMethod->getParameters());
4463 3
                while (count($args) < $argumentCount - 1) {
4464 1
                    $args[] = null;
4465
                }
4466
            }
4467
4468 3
            $args[] = $pCell;
4469
        }
4470
4471 48
        return $args;
4472
    }
4473
}
4474