Completed
Push — develop ( 4fd8e7...d3e769 )
by Adrien
14:12 queued 07:04
created

Calculation::getLocale()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 0
cts 4
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Category;
6
use PhpOffice\PhpSpreadsheet\Calculation\Database;
7
use PhpOffice\PhpSpreadsheet\Calculation\DateTime;
8
use PhpOffice\PhpSpreadsheet\Calculation\Engineering;
9
use PhpOffice\PhpSpreadsheet\Calculation\Financial;
10
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
11
use PhpOffice\PhpSpreadsheet\Calculation\Logical;
12
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
13
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
14
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
15
use PhpOffice\PhpSpreadsheet\Calculation\TextData;
16
use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack;
17
18
class Calculation
19
{
20
    /** Constants                */
21
    /** Regular Expressions        */
22
    //    Numeric operand
23
    const CALCULATION_REGEXP_NUMBER = '[-+]?\d*\.?\d+(e[-+]?\d+)?';
24
    //    String operand
25
    const CALCULATION_REGEXP_STRING = '"(?:[^"]|"")*"';
26
    //    Opening bracket
27
    const CALCULATION_REGEXP_OPENBRACE = '\(';
28
    //    Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
29
    const CALCULATION_REGEXP_FUNCTION = '@?([A-Z][A-Z0-9\.]*)[\s]*\(';
30
    //    Cell reference (cell or range of cells, with or without a sheet reference)
31
    const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?([a-z]{1,3})\$?(\d{1,7})';
32
    //    Named Range of cells
33
    const CALCULATION_REGEXP_NAMEDRANGE = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?([_A-Z][_A-Z0-9\.]*)';
34
    //    Error
35
    const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?';
36
37
    /** constants */
38
    const RETURN_ARRAY_AS_ERROR = 'error';
39
    const RETURN_ARRAY_AS_VALUE = 'value';
40
    const RETURN_ARRAY_AS_ARRAY = 'array';
41
42
    private static $returnArrayAsType = self::RETURN_ARRAY_AS_VALUE;
43
44
    /**
45
     * Instance of this class.
46
     *
47
     * @var \PhpOffice\PhpSpreadsheet\Calculation
48
     */
49
    private static $instance;
50
51
    /**
52
     * Instance of the spreadsheet this Calculation Engine is using.
53
     *
54
     * @var PhpSpreadsheet
55
     */
56
    private $spreadsheet;
57
58
    /**
59
     * Calculation cache.
60
     *
61
     * @var array
62
     */
63
    private $calculationCache = [];
64
65
    /**
66
     * Calculation cache enabled.
67
     *
68
     * @var bool
69
     */
70
    private $calculationCacheEnabled = true;
71
72
    /**
73
     * List of operators that can be used within formulae
74
     * The true/false value indicates whether it is a binary operator or a unary operator.
75
     *
76
     * @var array
77
     */
78
    private static $operators = [
79
        '+' => true, '-' => true, '*' => true, '/' => true,
80
        '^' => true, '&' => true, '%' => false, '~' => false,
81
        '>' => true, '<' => true, '=' => true, '>=' => true,
82
        '<=' => true, '<>' => true, '|' => true, ':' => true,
83
    ];
84
85
    /**
86
     * List of binary operators (those that expect two operands).
87
     *
88
     * @var array
89
     */
90
    private static $binaryOperators = [
91
        '+' => true, '-' => true, '*' => true, '/' => true,
92
        '^' => true, '&' => true, '>' => true, '<' => true,
93
        '=' => true, '>=' => true, '<=' => true, '<>' => true,
94
        '|' => true, ':' => true,
95
    ];
96
97
    /**
98
     * The debug log generated by the calculation engine.
99
     *
100
     * @var CalcEngine\Logger
101
     */
102
    private $debugLog;
103
104
    /**
105
     * Flag to determine how formula errors should be handled
106
     *        If true, then a user error will be triggered
107
     *        If false, then an exception will be thrown.
108
     *
109
     * @var bool
110
     */
111
    public $suppressFormulaErrors = false;
112
113
    /**
114
     * Error message for any error that was raised/thrown by the calculation engine.
115
     *
116
     * @var string
117
     */
118
    public $formulaError;
119
120
    /**
121
     * An array of the nested cell references accessed by the calculation engine, used for the debug log.
122
     *
123
     * @var array of string
124
     */
125
    private $cyclicReferenceStack;
126
127
    private $cellStack = [];
128
129
    /**
130
     * Current iteration counter for cyclic formulae
131
     * If the value is 0 (or less) then cyclic formulae will throw an exception,
132
     * otherwise they will iterate to the limit defined here before returning a result.
133
     *
134
     * @var int
135
     */
136
    private $cyclicFormulaCounter = 1;
137
138
    private $cyclicFormulaCell = '';
139
140
    /**
141
     * Number of iterations for cyclic formulae.
142
     *
143
     * @var int
144
     */
145
    public $cyclicFormulaCount = 1;
146
147
    /**
148
     * Epsilon Precision used for comparisons in calculations.
149
     *
150
     * @var float
151
     */
152
    private $delta = 0.1e-12;
153
154
    /**
155
     * The current locale setting.
156
     *
157
     * @var string
158
     */
159
    private static $localeLanguage = 'en_us'; //    US English    (default locale)
160
161
    /**
162
     * List of available locale settings
163
     * Note that this is read for the locale subdirectory only when requested.
164
     *
165
     * @var string[]
166
     */
167
    private static $validLocaleLanguages = [
168
        'en', //    English        (default language)
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1999
2000
        $this->spreadsheet = $spreadsheet;
0 ignored issues
show
Documentation Bug introduced by
It seems like $spreadsheet can also be of type object<PhpOffice\PhpSpreadsheet\Spreadsheet>. However, the property $spreadsheet is declared as type object<PhpOffice\PhpSpreadsheet\PhpSpreadsheet>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2001
        $this->cyclicReferenceStack = new CalcEngine\CyclicReferenceStack();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \PhpOffice\PhpSpread...\CyclicReferenceStack() of type object<PhpOffice\PhpSpre...e\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...
2002
        $this->debugLog = new CalcEngine\Logger($this->cyclicReferenceStack);
2003
    }
2004
2005
    private static function loadLocales()
2006
    {
2007
        $localeFileDirectory = __DIR__ . '/locale/';
2008
        foreach (glob($localeFileDirectory . '/*', GLOB_ONLYDIR) as $filename) {
2009
            $filename = substr($filename, strlen($localeFileDirectory) + 1);
2010
            if ($filename != 'en') {
2011
                self::$validLocaleLanguages[] = $filename;
2012
            }
2013
        }
2014
    }
2015
2016
    /**
2017
     * Get an instance of this class.
2018
     *
2019
     * @param Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object,
2020
     *                                    or NULL to create a standalone claculation engine
2021
     *
2022
     * @return Calculation
2023
     */
2024
    public static function getInstance(Spreadsheet $spreadsheet = null)
2025
    {
2026
        if ($spreadsheet !== null) {
2027
            $instance = $spreadsheet->getCalculationEngine();
2028
            if (isset($instance)) {
2029
                return $instance;
2030
            }
2031
        }
2032
2033
        if (!isset(self::$instance) || (self::$instance === null)) {
2034
            self::$instance = new self();
2035
        }
2036
2037
        return self::$instance;
2038
    }
2039
2040
    /**
2041
     * Unset an instance of this class.
2042
     */
2043
    public function __destruct()
2044
    {
2045
        $this->workbook = null;
0 ignored issues
show
Bug introduced by
The property workbook does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2046
    }
2047
2048
    /**
2049
     * Flush the calculation cache for any existing instance of this class
2050
     *        but only if a Calculation instance exists.
2051
     */
2052
    public function flushInstance()
2053
    {
2054
        $this->clearCalculationCache();
2055
    }
2056
2057
    /**
2058
     * Get the Logger for this calculation engine instance.
2059
     *
2060
     * @return CalcEngine\Logger
2061
     */
2062
    public function getDebugLog()
2063
    {
2064
        return $this->debugLog;
2065
    }
2066
2067
    /**
2068
     * __clone implementation. Cloning should not be allowed in a Singleton!
2069
     *
2070
     * @throws Calculation\Exception
2071
     */
2072
    final public function __clone()
2073
    {
2074
        throw new Calculation\Exception('Cloning the calculation engine is not allowed!');
2075
    }
2076
2077
    /**
2078
     * Return the locale-specific translation of TRUE.
2079
     *
2080
     * @return string locale-specific translation of TRUE
2081
     */
2082
    public static function getTRUE()
2083
    {
2084
        return self::$localeBoolean['TRUE'];
2085
    }
2086
2087
    /**
2088
     * Return the locale-specific translation of FALSE.
2089
     *
2090
     * @return string locale-specific translation of FALSE
2091
     */
2092
    public static function getFALSE()
2093
    {
2094
        return self::$localeBoolean['FALSE'];
2095
    }
2096
2097
    /**
2098
     * Set the Array Return Type (Array or Value of first element in the array).
2099
     *
2100
     * @param string $returnType Array return type
2101
     *
2102
     * @return bool Success or failure
2103
     */
2104
    public static function setArrayReturnType($returnType)
2105
    {
2106
        if (($returnType == self::RETURN_ARRAY_AS_VALUE) ||
2107
            ($returnType == self::RETURN_ARRAY_AS_ERROR) ||
2108
            ($returnType == self::RETURN_ARRAY_AS_ARRAY)) {
2109
            self::$returnArrayAsType = $returnType;
2110
2111
            return true;
2112
        }
2113
2114
        return false;
2115
    }
2116
2117
    /**
2118
     * Return the Array Return Type (Array or Value of first element in the array).
2119
     *
2120
     * @return string $returnType Array return type
2121
     */
2122
    public static function getArrayReturnType()
2123
    {
2124
        return self::$returnArrayAsType;
2125
    }
2126
2127
    /**
2128
     * Is calculation caching enabled?
2129
     *
2130
     * @return bool
2131
     */
2132
    public function getCalculationCacheEnabled()
2133
    {
2134
        return $this->calculationCacheEnabled;
2135
    }
2136
2137
    /**
2138
     * Enable/disable calculation cache.
2139
     *
2140
     * @param bool $pValue
2141
     */
2142
    public function setCalculationCacheEnabled($pValue)
2143
    {
2144
        $this->calculationCacheEnabled = $pValue;
2145
        $this->clearCalculationCache();
2146
    }
2147
2148
    /**
2149
     * Enable calculation cache.
2150
     */
2151
    public function enableCalculationCache()
2152
    {
2153
        $this->setCalculationCacheEnabled(true);
2154
    }
2155
2156
    /**
2157
     * Disable calculation cache.
2158
     */
2159
    public function disableCalculationCache()
2160
    {
2161
        $this->setCalculationCacheEnabled(false);
2162
    }
2163
2164
    /**
2165
     * Clear calculation cache.
2166
     */
2167
    public function clearCalculationCache()
2168
    {
2169
        $this->calculationCache = [];
2170
    }
2171
2172
    /**
2173
     * Clear calculation cache for a specified worksheet.
2174
     *
2175
     * @param string $worksheetName
2176
     */
2177
    public function clearCalculationCacheForWorksheet($worksheetName)
2178
    {
2179
        if (isset($this->calculationCache[$worksheetName])) {
2180
            unset($this->calculationCache[$worksheetName]);
2181
        }
2182
    }
2183
2184
    /**
2185
     * Rename calculation cache for a specified worksheet.
2186
     *
2187
     * @param string $fromWorksheetName
2188
     * @param string $toWorksheetName
2189
     */
2190
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName)
2191
    {
2192
        if (isset($this->calculationCache[$fromWorksheetName])) {
2193
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2194
            unset($this->calculationCache[$fromWorksheetName]);
2195
        }
2196
    }
2197
2198
    /**
2199
     * Get the currently defined locale code.
2200
     *
2201
     * @return string
2202
     */
2203
    public function getLocale()
2204
    {
2205
        return self::$localeLanguage;
2206
    }
2207
2208
    /**
2209
     * Set the locale code.
2210
     *
2211
     * @param string $locale The locale to use for formula translation, eg: 'en_us'
2212
     *
2213
     * @return bool
2214
     */
2215
    public function setLocale($locale)
2216
    {
2217
        //    Identify our locale and language
2218
        $language = $locale = strtolower($locale);
2219
        if (strpos($locale, '_') !== false) {
2220
            [$language] = explode('_', $locale);
2221
        }
2222
2223
        if (count(self::$validLocaleLanguages) == 1) {
2224
            self::loadLocales();
2225
        }
2226
        //    Test whether we have any language data for this language (any locale)
2227
        if (in_array($language, self::$validLocaleLanguages)) {
2228
            //    initialise language/locale settings
2229
            self::$localeFunctions = [];
2230
            self::$localeArgumentSeparator = ',';
2231
            self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL'];
0 ignored issues
show
Documentation Bug introduced by
It seems like array('TRUE' => 'TRUE', ...LSE', 'NULL' => 'NULL') of type array<string,string,{"TR...ring","NULL":"string"}> is incompatible with the declared type array<integer,string> of property $localeBoolean.

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...
2232
            //    Default is English, if user isn't requesting english, then read the necessary data from the locale files
2233
            if ($locale != 'en_us') {
2234
                //    Search for a file with a list of function names for locale
2235
                $functionNamesFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'functions';
2236
                if (!file_exists($functionNamesFile)) {
2237
                    //    If there isn't a locale specific function file, look for a language specific function file
2238
                    $functionNamesFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'functions';
2239
                    if (!file_exists($functionNamesFile)) {
2240
                        return false;
2241
                    }
2242
                }
2243
                //    Retrieve the list of locale or language specific function names
2244
                $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2245
                foreach ($localeFunctions as $localeFunction) {
2246
                    [$localeFunction] = explode('##', $localeFunction); //    Strip out comments
2247
                    if (strpos($localeFunction, '=') !== false) {
2248
                        [$fName, $lfName] = explode('=', $localeFunction);
0 ignored issues
show
Bug introduced by
The variable $fName does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $lfName does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2249
                        $fName = trim($fName);
2250
                        $lfName = trim($lfName);
2251
                        if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) {
2252
                            self::$localeFunctions[$fName] = $lfName;
2253
                        }
2254
                    }
2255
                }
2256
                //    Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions
2257
                if (isset(self::$localeFunctions['TRUE'])) {
2258
                    self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE'];
2259
                }
2260
                if (isset(self::$localeFunctions['FALSE'])) {
2261
                    self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE'];
2262
                }
2263
2264
                $configFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'config';
2265
                if (!file_exists($configFile)) {
2266
                    $configFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'config';
2267
                }
2268
                if (file_exists($configFile)) {
2269
                    $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2270
                    foreach ($localeSettings as $localeSetting) {
2271
                        [$localeSetting] = explode('##', $localeSetting); //    Strip out comments
2272
                        if (strpos($localeSetting, '=') !== false) {
2273
                            [$settingName, $settingValue] = explode('=', $localeSetting);
0 ignored issues
show
Bug introduced by
The variable $settingName does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $settingValue does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
2274
                            $settingName = strtoupper(trim($settingName));
2275
                            switch ($settingName) {
2276
                                case 'ARGUMENTSEPARATOR':
2277
                                    self::$localeArgumentSeparator = trim($settingValue);
2278
2279
                                    break;
2280
                            }
2281
                        }
2282
                    }
2283
                }
2284
            }
2285
2286
            self::$functionReplaceFromExcel = self::$functionReplaceToExcel =
2287
            self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
2288
            self::$localeLanguage = $locale;
2289
2290
            return true;
2291
        }
2292
2293
        return false;
2294
    }
2295
2296
    /**
2297
     * @param string $fromSeparator
2298
     * @param string $toSeparator
2299
     * @param string $formula
2300
     * @param bool $inBraces
2301
     *
2302
     * @return string
2303
     */
2304
    public static function translateSeparator($fromSeparator, $toSeparator, $formula, &$inBraces)
2305
    {
2306
        $strlen = mb_strlen($formula);
2307
        for ($i = 0; $i < $strlen; ++$i) {
2308
            $chr = mb_substr($formula, $i, 1);
2309
            switch ($chr) {
2310
                case '{':
2311
                    $inBraces = true;
2312
2313
                    break;
2314
                case '}':
2315
                    $inBraces = false;
2316
2317
                    break;
2318
                case $fromSeparator:
2319
                    if (!$inBraces) {
2320
                        $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
2321
                    }
2322
            }
2323
        }
2324
2325
        return $formula;
2326
    }
2327
2328
    /**
2329
     * @param string $fromSeparator
2330
     * @param string $toSeparator
2331
     * @param mixed $from
2332
     * @param mixed $to
2333
     * @param mixed $formula
2334
     */
2335
    private static function translateFormula($from, $to, $formula, $fromSeparator, $toSeparator)
2336
    {
2337
        //    Convert any Excel function names to the required language
2338
        if (self::$localeLanguage !== 'en_us') {
2339
            $inBraces = false;
2340
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
2341
            if (strpos($formula, '"') !== false) {
2342
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
2343
                //        the formula
2344
                $temp = explode('"', $formula);
2345
                $i = false;
2346
                foreach ($temp as &$value) {
2347
                    //    Only count/replace in alternating array entries
2348
                    if ($i = !$i) {
2349
                        $value = preg_replace($from, $to, $value);
2350
                        $value = self::translateSeparator($fromSeparator, $toSeparator, $value, $inBraces);
2351
                    }
2352
                }
2353
                unset($value);
2354
                //    Then rebuild the formula string
2355
                $formula = implode('"', $temp);
2356
            } else {
2357
                //    If there's no quoted strings, then we do a simple count/replace
2358
                $formula = preg_replace($from, $to, $formula);
2359
                $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inBraces);
0 ignored issues
show
Bug introduced by
It seems like $formula can also be of type array<integer,string>; however, PhpOffice\PhpSpreadsheet...n::translateSeparator() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2360
            }
2361
        }
2362
2363
        return $formula;
2364
    }
2365
2366
    private static $functionReplaceFromExcel = null;
2367
    private static $functionReplaceToLocale = null;
2368
2369
    public function _translateFormulaToLocale($formula)
2370
    {
2371
        if (self::$functionReplaceFromExcel === null) {
2372
            self::$functionReplaceFromExcel = [];
2373
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2374
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName) . '([\s]*\()/Ui';
2375
            }
2376
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2377
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean) . '([^\w\.])/Ui';
2378
            }
2379
        }
2380
2381
        if (self::$functionReplaceToLocale === null) {
2382
            self::$functionReplaceToLocale = [];
2383
            foreach (array_values(self::$localeFunctions) as $localeFunctionName) {
2384
                self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2';
2385
            }
2386
            foreach (array_values(self::$localeBoolean) as $localeBoolean) {
2387
                self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2';
2388
            }
2389
        }
2390
2391
        return self::translateFormula(self::$functionReplaceFromExcel, self::$functionReplaceToLocale, $formula, ',', self::$localeArgumentSeparator);
2392
    }
2393
2394
    private static $functionReplaceFromLocale = null;
2395
    private static $functionReplaceToExcel = null;
2396
2397
    public function _translateFormulaToEnglish($formula)
2398
    {
2399
        if (self::$functionReplaceFromLocale === null) {
2400
            self::$functionReplaceFromLocale = [];
2401
            foreach (array_values(self::$localeFunctions) as $localeFunctionName) {
2402
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName) . '([\s]*\()/Ui';
2403
            }
2404
            foreach (array_values(self::$localeBoolean) as $excelBoolean) {
2405
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean) . '([^\w\.])/Ui';
2406
            }
2407
        }
2408
2409
        if (self::$functionReplaceToExcel === null) {
2410
            self::$functionReplaceToExcel = [];
2411
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2412
                self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2';
2413
            }
2414
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2415
                self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2';
2416
            }
2417
        }
2418
2419
        return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ',');
2420
    }
2421
2422
    public static function localeFunc($function)
2423
    {
2424
        if (self::$localeLanguage !== 'en_us') {
2425
            $functionName = trim($function, '(');
2426
            if (isset(self::$localeFunctions[$functionName])) {
2427
                $brace = ($functionName != $function);
2428
                $function = self::$localeFunctions[$functionName];
2429
                if ($brace) {
2430
                    $function .= '(';
2431
                }
2432
            }
2433
        }
2434
2435
        return $function;
2436
    }
2437
2438
    /**
2439
     * Wrap string values in quotes.
2440
     *
2441
     * @param mixed $value
2442
     *
2443
     * @return mixed
2444
     */
2445
    public static function wrapResult($value)
2446
    {
2447
        if (is_string($value)) {
2448
            //    Error values cannot be "wrapped"
2449
            if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
2450
                //    Return Excel errors "as is"
2451
                return $value;
2452
            }
2453
            //    Return strings wrapped in quotes
2454
            return '"' . $value . '"';
2455
            //    Convert numeric errors to NaN error
2456
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2457
            return Calculation\Functions::NAN();
2458
        }
2459
2460
        return $value;
2461
    }
2462
2463
    /**
2464
     * Remove quotes used as a wrapper to identify string values.
2465
     *
2466
     * @param mixed $value
2467
     *
2468
     * @return mixed
2469
     */
2470
    public static function unwrapResult($value)
2471
    {
2472
        if (is_string($value)) {
2473
            if ((isset($value[0])) && ($value[0] == '"') && (substr($value, -1) == '"')) {
2474
                return substr($value, 1, -1);
2475
            }
2476
            //    Convert numeric errors to NAN error
2477
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2478
            return Calculation\Functions::NAN();
2479
        }
2480
2481
        return $value;
2482
    }
2483
2484
    /**
2485
     * Calculate cell value (using formula from a cell ID)
2486
     * Retained for backward compatibility.
2487
     *
2488
     * @param Cell $pCell Cell to calculate
2489
     *
2490
     * @throws Calculation\Exception
2491
     *
2492
     * @return mixed
2493
     */
2494
    public function calculate(Cell $pCell = null)
2495
    {
2496
        try {
2497
            return $this->calculateCellValue($pCell);
2498
        } catch (Exception $e) {
2499
            throw new Calculation\Exception($e->getMessage());
2500
        }
2501
    }
2502
2503
    /**
2504
     * Calculate the value of a cell formula.
2505
     *
2506
     * @param Cell $pCell Cell to calculate
2507
     * @param bool $resetLog Flag indicating whether the debug log should be reset or not
2508
     *
2509
     * @throws Calculation\Exception
2510
     *
2511
     * @return mixed
2512
     */
2513
    public function calculateCellValue(Cell $pCell = null, $resetLog = true)
2514
    {
2515
        if ($pCell === null) {
2516
            return null;
2517
        }
2518
2519
        $returnArrayAsType = self::$returnArrayAsType;
2520
        if ($resetLog) {
2521
            //    Initialise the logging settings if requested
2522
            $this->formulaError = null;
2523
            $this->debugLog->clearLog();
2524
            $this->cyclicReferenceStack->clear();
0 ignored issues
show
Bug introduced by
The method clear cannot be called on $this->cyclicReferenceStack (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2525
            $this->cyclicFormulaCounter = 1;
2526
2527
            self::$returnArrayAsType = self::RETURN_ARRAY_AS_ARRAY;
2528
        }
2529
2530
        //    Execute the calculation for the cell formula
2531
        $this->cellStack[] = [
2532
            'sheet' => $pCell->getWorksheet()->getTitle(),
2533
            'cell' => $pCell->getCoordinate(),
2534
        ];
2535
2536
        try {
2537
            $result = self::unwrapResult($this->_calculateFormulaValue($pCell->getValue(), $pCell->getCoordinate(), $pCell));
2538
            $cellAddress = array_pop($this->cellStack);
2539
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2540
        } catch (Exception $e) {
2541
            $cellAddress = array_pop($this->cellStack);
2542
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2543
2544
            throw new Calculation\Exception($e->getMessage());
2545
        }
2546
2547
        if ((is_array($result)) && (self::$returnArrayAsType != self::RETURN_ARRAY_AS_ARRAY)) {
2548
            self::$returnArrayAsType = $returnArrayAsType;
2549
            $testResult = Calculation\Functions::flattenArray($result);
2550
            if (self::$returnArrayAsType == self::RETURN_ARRAY_AS_ERROR) {
2551
                return Calculation\Functions::VALUE();
2552
            }
2553
            //    If there's only a single cell in the array, then we allow it
2554
            if (count($testResult) != 1) {
2555
                //    If keys are numeric, then it's a matrix result rather than a cell range result, so we permit it
2556
                $r = array_keys($result);
2557
                $r = array_shift($r);
2558
                if (!is_numeric($r)) {
2559
                    return Calculation\Functions::VALUE();
2560
                }
2561
                if (is_array($result[$r])) {
2562
                    $c = array_keys($result[$r]);
2563
                    $c = array_shift($c);
2564
                    if (!is_numeric($c)) {
2565
                        return Calculation\Functions::VALUE();
2566
                    }
2567
                }
2568
            }
2569
            $result = array_shift($testResult);
2570
        }
2571
        self::$returnArrayAsType = $returnArrayAsType;
2572
2573
        if ($result === null) {
2574
            return 0;
2575
        } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
2576
            return Calculation\Functions::NAN();
2577
        }
2578
2579
        return $result;
2580
    }
2581
2582
    /**
2583
     * Validate and parse a formula string.
2584
     *
2585
     * @param string $formula Formula to parse
2586
     *
2587
     * @throws Calculation\Exception
2588
     *
2589
     * @return array
2590
     */
2591
    public function parseFormula($formula)
2592
    {
2593
        //    Basic validation that this is indeed a formula
2594
        //    We return an empty array if not
2595
        $formula = trim($formula);
2596
        if ((!isset($formula[0])) || ($formula[0] != '=')) {
2597
            return [];
2598
        }
2599
        $formula = ltrim(substr($formula, 1));
2600
        if (!isset($formula[0])) {
2601
            return [];
2602
        }
2603
2604
        //    Parse the formula and return the token stack
2605
        return $this->_parseFormula($formula);
2606
    }
2607
2608
    /**
2609
     * Calculate the value of a formula.
2610
     *
2611
     * @param string $formula Formula to parse
2612
     * @param string $cellID Address of the cell to calculate
2613
     * @param Cell $pCell Cell to calculate
2614
     *
2615
     * @throws Calculation\Exception
2616
     *
2617
     * @return mixed
2618
     */
2619
    public function calculateFormula($formula, $cellID = null, Cell $pCell = null)
2620
    {
2621
        //    Initialise the logging settings
2622
        $this->formulaError = null;
2623
        $this->debugLog->clearLog();
2624
        $this->cyclicReferenceStack->clear();
0 ignored issues
show
Bug introduced by
The method clear cannot be called on $this->cyclicReferenceStack (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2625
2626
        if ($this->spreadsheet !== null && $cellID === null && $pCell === null) {
2627
            $cellID = 'A1';
2628
            $pCell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
2629
        } else {
2630
            //    Disable calculation cacheing because it only applies to cell calculations, not straight formulae
2631
            //    But don't actually flush any cache
2632
            $resetCache = $this->getCalculationCacheEnabled();
2633
            $this->calculationCacheEnabled = false;
2634
        }
2635
2636
        //    Execute the calculation
2637
        try {
2638
            $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $pCell));
2639
        } catch (Exception $e) {
2640
            throw new Calculation\Exception($e->getMessage());
2641
        }
2642
2643
        if ($this->spreadsheet === null) {
2644
            //    Reset calculation cacheing to its previous state
2645
            $this->calculationCacheEnabled = $resetCache;
0 ignored issues
show
Bug introduced by
The variable $resetCache does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2646
        }
2647
2648
        return $result;
2649
    }
2650
2651
    /**
2652
     * @param string $cellReference
2653
     * @param mixed $cellValue
2654
     *
2655
     * @return bool
2656
     */
2657
    public function getValueFromCache($cellReference, &$cellValue)
2658
    {
2659
        // Is calculation cacheing enabled?
2660
        // Is the value present in calculation cache?
2661
        $this->debugLog->writeDebugLog('Testing cache value for cell ', $cellReference);
2662
        if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
2663
            $this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache');
2664
            // Return the cached result
2665
            $cellValue = $this->calculationCache[$cellReference];
2666
2667
            return true;
2668
        }
2669
2670
        return false;
2671
    }
2672
2673
    /**
2674
     * @param string $cellReference
2675
     * @param mixed $cellValue
2676
     */
2677
    public function saveValueToCache($cellReference, $cellValue)
2678
    {
2679
        if ($this->calculationCacheEnabled) {
2680
            $this->calculationCache[$cellReference] = $cellValue;
2681
        }
2682
    }
2683
2684
    /**
2685
     * Parse a cell formula and calculate its value.
2686
     *
2687
     * @param string $formula The formula to parse and calculate
2688
     * @param string $cellID The ID (e.g. A3) of the cell that we are calculating
2689
     * @param Cell $pCell Cell to calculate
2690
     *
2691
     * @throws Calculation\Exception
2692
     *
2693
     * @return mixed
2694
     */
2695
    public function _calculateFormulaValue($formula, $cellID = null, Cell $pCell = null)
2696
    {
2697
        $cellValue = null;
2698
2699
        //    Basic validation that this is indeed a formula
2700
        //    We simply return the cell value if not
2701
        $formula = trim($formula);
2702
        if ($formula[0] != '=') {
2703
            return self::wrapResult($formula);
2704
        }
2705
        $formula = ltrim(substr($formula, 1));
2706
        if (!isset($formula[0])) {
2707
            return self::wrapResult($formula);
2708
        }
2709
2710
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
2711
        $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
2712
        $wsCellReference = $wsTitle . '!' . $cellID;
2713
2714
        if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
2715
            return $cellValue;
2716
        }
2717
2718
        if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) {
0 ignored issues
show
Bug introduced by
The method onStack cannot be called on $this->cyclicReferenceStack (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2719
            if ($this->cyclicFormulaCount <= 0) {
2720
                $this->cyclicFormulaCell = '';
2721
2722
                return $this->raiseFormulaError('Cyclic Reference in Formula');
2723
            } elseif ($this->cyclicFormulaCell === $wsCellReference) {
2724
                ++$this->cyclicFormulaCounter;
2725
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2726
                    $this->cyclicFormulaCell = '';
2727
2728
                    return $cellValue;
2729
                }
2730
            } elseif ($this->cyclicFormulaCell == '') {
2731
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2732
                    return $cellValue;
2733
                }
2734
                $this->cyclicFormulaCell = $wsCellReference;
2735
            }
2736
        }
2737
2738
        //    Parse the formula onto the token stack and calculate the value
2739
        $this->cyclicReferenceStack->push($wsCellReference);
0 ignored issues
show
Bug introduced by
The method push cannot be called on $this->cyclicReferenceStack (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2740
        $cellValue = $this->processTokenStack($this->_parseFormula($formula, $pCell), $cellID, $pCell);
2741
        $this->cyclicReferenceStack->pop();
0 ignored issues
show
Bug introduced by
The method pop cannot be called on $this->cyclicReferenceStack (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2742
2743
        // Save to calculation cache
2744
        if ($cellID !== null) {
2745
            $this->saveValueToCache($wsCellReference, $cellValue);
2746
        }
2747
2748
        //    Return the calculated value
2749
        return $cellValue;
2750
    }
2751
2752
    /**
2753
     * Ensure that paired matrix operands are both matrices and of the same size.
2754
     *
2755
     * @param mixed &$operand1 First matrix operand
2756
     * @param mixed &$operand2 Second matrix operand
2757
     * @param int $resize Flag indicating whether the matrices should be resized to match
2758
     *                                        and (if so), whether the smaller dimension should grow or the
2759
     *                                        larger should shrink.
2760
     *                                            0 = no resize
2761
     *                                            1 = shrink to fit
2762
     *                                            2 = extend to fit
2763
     */
2764
    private static function checkMatrixOperands(&$operand1, &$operand2, $resize = 1)
2765
    {
2766
        //    Examine each of the two operands, and turn them into an array if they aren't one already
2767
        //    Note that this function should only be called if one or both of the operand is already an array
2768
        if (!is_array($operand1)) {
2769
            [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand2);
0 ignored issues
show
Bug introduced by
The variable $matrixRows does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $matrixColumns does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
2770
            $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1));
2771
            $resize = 0;
2772
        } elseif (!is_array($operand2)) {
2773
            [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand1);
2774
            $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2));
2775
            $resize = 0;
2776
        }
2777
2778
        [$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1);
0 ignored issues
show
Bug introduced by
The variable $matrix1Rows does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $matrix1Columns does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
2779
        [$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2);
0 ignored issues
show
Bug introduced by
The variable $matrix2Rows does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $matrix2Columns does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
2780
        if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
2781
            $resize = 1;
2782
        }
2783
2784
        if ($resize == 2) {
2785
            //    Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger
2786
            self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2787
        } elseif ($resize == 1) {
2788
            //    Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller
2789
            self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2790
        }
2791
2792
        return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns];
2793
    }
2794
2795
    /**
2796
     * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0.
2797
     *
2798
     * @param mixed &$matrix matrix operand
2799
     *
2800
     * @return int[] An array comprising the number of rows, and number of columns
2801
     */
2802
    private static function getMatrixDimensions(&$matrix)
2803
    {
2804
        $matrixRows = count($matrix);
2805
        $matrixColumns = 0;
2806
        foreach ($matrix as $rowKey => $rowValue) {
2807
            $matrixColumns = max(count($rowValue), $matrixColumns);
2808
            if (!is_array($rowValue)) {
2809
                $matrix[$rowKey] = [$rowValue];
2810
            } else {
2811
                $matrix[$rowKey] = array_values($rowValue);
2812
            }
2813
        }
2814
        $matrix = array_values($matrix);
2815
2816
        return [$matrixRows, $matrixColumns];
2817
    }
2818
2819
    /**
2820
     * Ensure that paired matrix operands are both matrices of the same size.
2821
     *
2822
     * @param mixed &$matrix1 First matrix operand
2823
     * @param mixed &$matrix2 Second matrix operand
2824
     * @param int $matrix1Rows Row size of first matrix operand
2825
     * @param int $matrix1Columns Column size of first matrix operand
2826
     * @param int $matrix2Rows Row size of second matrix operand
2827
     * @param int $matrix2Columns Column size of second matrix operand
2828
     */
2829
    private static function resizeMatricesShrink(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
2830
    {
2831
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
2832
            if ($matrix2Rows < $matrix1Rows) {
2833
                for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) {
2834
                    unset($matrix1[$i]);
2835
                }
2836
            }
2837
            if ($matrix2Columns < $matrix1Columns) {
2838
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2839
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
2840
                        unset($matrix1[$i][$j]);
2841
                    }
2842
                }
2843
            }
2844
        }
2845
2846
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
2847
            if ($matrix1Rows < $matrix2Rows) {
2848
                for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) {
2849
                    unset($matrix2[$i]);
2850
                }
2851
            }
2852
            if ($matrix1Columns < $matrix2Columns) {
2853
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2854
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
2855
                        unset($matrix2[$i][$j]);
2856
                    }
2857
                }
2858
            }
2859
        }
2860
    }
2861
2862
    /**
2863
     * Ensure that paired matrix operands are both matrices of the same size.
2864
     *
2865
     * @param mixed &$matrix1 First matrix operand
2866
     * @param mixed &$matrix2 Second matrix operand
2867
     * @param int $matrix1Rows Row size of first matrix operand
2868
     * @param int $matrix1Columns Column size of first matrix operand
2869
     * @param int $matrix2Rows Row size of second matrix operand
2870
     * @param int $matrix2Columns Column size of second matrix operand
2871
     */
2872
    private static function resizeMatricesExtend(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
2873
    {
2874
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
2875
            if ($matrix2Columns < $matrix1Columns) {
2876
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2877
                    $x = $matrix2[$i][$matrix2Columns - 1];
2878
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
2879
                        $matrix2[$i][$j] = $x;
2880
                    }
2881
                }
2882
            }
2883
            if ($matrix2Rows < $matrix1Rows) {
2884
                $x = $matrix2[$matrix2Rows - 1];
2885
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2886
                    $matrix2[$i] = $x;
2887
                }
2888
            }
2889
        }
2890
2891
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
2892
            if ($matrix1Columns < $matrix2Columns) {
2893
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2894
                    $x = $matrix1[$i][$matrix1Columns - 1];
2895
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
2896
                        $matrix1[$i][$j] = $x;
2897
                    }
2898
                }
2899
            }
2900
            if ($matrix1Rows < $matrix2Rows) {
2901
                $x = $matrix1[$matrix1Rows - 1];
2902
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2903
                    $matrix1[$i] = $x;
2904
                }
2905
            }
2906
        }
2907
    }
2908
2909
    /**
2910
     * Format details of an operand for display in the log (based on operand type).
2911
     *
2912
     * @param mixed $value First matrix operand
2913
     *
2914
     * @return mixed
2915
     */
2916
    private function showValue($value)
2917
    {
2918
        if ($this->debugLog->getWriteDebugLog()) {
2919
            $testArray = Calculation\Functions::flattenArray($value);
2920
            if (count($testArray) == 1) {
2921
                $value = array_pop($testArray);
2922
            }
2923
2924
            if (is_array($value)) {
2925
                $returnMatrix = [];
2926
                $pad = $rpad = ', ';
2927
                foreach ($value as $row) {
2928
                    if (is_array($row)) {
2929
                        $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
2930
                        $rpad = '; ';
2931
                    } else {
2932
                        $returnMatrix[] = $this->showValue($row);
2933
                    }
2934
                }
2935
2936
                return '{ ' . implode($rpad, $returnMatrix) . ' }';
2937
            } elseif (is_string($value) && (trim($value, '"') == $value)) {
2938
                return '"' . $value . '"';
2939
            } elseif (is_bool($value)) {
2940
                return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
2941
            }
2942
        }
2943
2944
        return Calculation\Functions::flattenSingleValue($value);
2945
    }
2946
2947
    /**
2948
     * Format type and details of an operand for display in the log (based on operand type).
2949
     *
2950
     * @param mixed $value First matrix operand
2951
     *
2952
     * @return null|string
2953
     */
2954
    private function showTypeDetails($value)
2955
    {
2956
        if ($this->debugLog->getWriteDebugLog()) {
2957
            $testArray = Calculation\Functions::flattenArray($value);
2958
            if (count($testArray) == 1) {
2959
                $value = array_pop($testArray);
2960
            }
2961
2962
            if ($value === null) {
2963
                return 'a NULL value';
2964
            } elseif (is_float($value)) {
2965
                $typeString = 'a floating point number';
2966
            } elseif (is_int($value)) {
2967
                $typeString = 'an integer number';
2968
            } elseif (is_bool($value)) {
2969
                $typeString = 'a boolean';
2970
            } elseif (is_array($value)) {
2971
                $typeString = 'a matrix';
2972
            } else {
2973
                if ($value == '') {
2974
                    return 'an empty string';
2975
                } elseif ($value[0] == '#') {
2976
                    return 'a ' . $value . ' error';
2977
                }
2978
                $typeString = 'a string';
2979
            }
2980
2981
            return $typeString . ' with a value of ' . $this->showValue($value);
2982
        }
2983
    }
2984
2985
    /**
2986
     * @param string $formula
2987
     *
2988
     * @return string
2989
     */
2990
    private function convertMatrixReferences($formula)
2991
    {
2992
        static $matrixReplaceFrom = ['{', ';', '}'];
2993
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
2994
2995
        //    Convert any Excel matrix references to the MKMATRIX() function
2996
        if (strpos($formula, '{') !== false) {
2997
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
2998
            if (strpos($formula, '"') !== false) {
2999
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
3000
                //        the formula
3001
                $temp = explode('"', $formula);
3002
                //    Open and Closed counts used for trapping mismatched braces in the formula
3003
                $openCount = $closeCount = 0;
3004
                $i = false;
3005
                foreach ($temp as &$value) {
3006
                    //    Only count/replace in alternating array entries
3007
                    if ($i = !$i) {
3008
                        $openCount += substr_count($value, '{');
3009
                        $closeCount += substr_count($value, '}');
3010
                        $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
3011
                    }
3012
                }
3013
                unset($value);
3014
                //    Then rebuild the formula string
3015
                $formula = implode('"', $temp);
3016
            } else {
3017
                //    If there's no quoted strings, then we do a simple count/replace
3018
                $openCount = substr_count($formula, '{');
3019
                $closeCount = substr_count($formula, '}');
3020
                $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
3021
            }
3022
            //    Trap for mismatched braces and trigger an appropriate error
3023
            if ($openCount < $closeCount) {
3024
                if ($openCount > 0) {
3025
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
3026
                }
3027
3028
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
3029
            } elseif ($openCount > $closeCount) {
3030
                if ($closeCount > 0) {
3031
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
3032
                }
3033
3034
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
3035
            }
3036
        }
3037
3038
        return $formula;
3039
    }
3040
3041
    private static function mkMatrix(...$args)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
3042
    {
3043
        return $args;
3044
    }
3045
3046
    //    Binary Operators
3047
    //    These operators always work on two values
3048
    //    Array key is the operator, the value indicates whether this is a left or right associative operator
3049
    private static $operatorAssociativity = [
3050
        '^' => 0, //    Exponentiation
3051
        '*' => 0, '/' => 0, //    Multiplication and Division
3052
        '+' => 0, '-' => 0, //    Addition and Subtraction
3053
        '&' => 0, //    Concatenation
3054
        '|' => 0, ':' => 0, //    Intersect and Range
3055
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3056
    ];
3057
3058
    //    Comparison (Boolean) Operators
3059
    //    These operators work on two values, but always return a boolean result
3060
    private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
3061
3062
    //    Operator Precedence
3063
    //    This list includes all valid operators, whether binary (including boolean) or unary (such as %)
3064
    //    Array key is the operator, the value is its precedence
3065
    private static $operatorPrecedence = [
3066
        ':' => 8, //    Range
3067
        '|' => 7, //    Intersect
3068
        '~' => 6, //    Negation
3069
        '%' => 5, //    Percentage
3070
        '^' => 4, //    Exponentiation
3071
        '*' => 3, '/' => 3, //    Multiplication and Division
3072
        '+' => 2, '-' => 2, //    Addition and Subtraction
3073
        '&' => 1, //    Concatenation
3074
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3075
    ];
3076
3077
    // Convert infix to postfix notation
3078
3079
    /**
3080
     * @param string $formula
3081
     * @param null|Cell $pCell
3082
     *
3083
     * @return bool
3084
     */
3085
    private function _parseFormula($formula, Cell $pCell = null)
3086
    {
3087
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
3088
            return false;
3089
        }
3090
3091
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
3092
        //        so we store the parent worksheet so that we can re-attach it when necessary
3093
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3094
3095
        $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION .
3096
                                '|' . self::CALCULATION_REGEXP_CELLREF .
3097
                                '|' . self::CALCULATION_REGEXP_NUMBER .
3098
                                '|' . self::CALCULATION_REGEXP_STRING .
3099
                                '|' . self::CALCULATION_REGEXP_OPENBRACE .
3100
                                '|' . self::CALCULATION_REGEXP_NAMEDRANGE .
3101
                                '|' . self::CALCULATION_REGEXP_ERROR .
3102
                                ')/si';
3103
3104
        //    Start with initialisation
3105
        $index = 0;
3106
        $stack = new Stack();
3107
        $output = [];
3108
        $expectingOperator = false; //    We use this test in syntax-checking the expression to determine when a
3109
                                                    //        - is a negation or + is a positive operator rather than an operation
3110
        $expectingOperand = false; //    We use this test in syntax-checking the expression to determine whether an operand
3111
                                                    //        should be null in a function call
3112
        //    The guts of the lexical parser
3113
        //    Loop through the formula extracting each operator and operand in turn
3114
        while (true) {
3115
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
3116
            if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
3117
                $opCharacter .= $formula[++$index];
3118
            }
3119
3120
            //    Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
3121
            $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
3122
3123
            if ($opCharacter == '-' && !$expectingOperator) {                //    Is it a negation instead of a minus?
3124
                $stack->push('Unary Operator', '~'); //    Put a negation on the stack
3125
                ++$index; //        and drop the negation symbol
3126
            } elseif ($opCharacter == '%' && $expectingOperator) {
3127
                $stack->push('Unary Operator', '%'); //    Put a percentage on the stack
3128
                ++$index;
3129
            } elseif ($opCharacter == '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
3130
                ++$index; //    Drop the redundant plus symbol
3131
            } elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) {    //    We have to explicitly deny a tilde or pipe, because they are legal
3132
                return $this->raiseFormulaError("Formula Error: Illegal character '~'"); //        on the stack but not in the input expression
3133
            } elseif ((isset(self::$operators[$opCharacter]) or $isOperandOrFunction) && $expectingOperator) {    //    Are we putting an operator on the stack?
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
3134
                while ($stack->count() > 0 &&
3135
                    ($o2 = $stack->last()) &&
3136
                    isset(self::$operators[$o2['value']]) &&
3137
                    @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3138
                    $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3139
                }
3140
                $stack->push('Binary Operator', $opCharacter); //    Finally put our current operator onto the stack
3141
                ++$index;
3142
                $expectingOperator = false;
3143
            } elseif ($opCharacter == ')' && $expectingOperator) {            //    Are we expecting to close a parenthesis?
3144
                $expectingOperand = false;
3145
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3146
                    if ($o2 === null) {
3147
                        return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"');
3148
                    }
3149
                    $output[] = $o2;
3150
                }
3151
                $d = $stack->last(2);
3152
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {    //    Did this parenthesis just close a function?
3153
                    $functionName = $matches[1]; //    Get the function name
3154
                    $d = $stack->pop();
3155
                    $argumentCount = $d['value']; //    See how many arguments there were (argument count is the next value stored on the stack)
3156
                    $output[] = $d; //    Dump the argument count on the output
3157
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
3158
                    if (isset(self::$controlFunctions[$functionName])) {
3159
                        $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
3160
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
0 ignored issues
show
Unused Code introduced by
$functionCall is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
3161
                    } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3162
                        $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
3163
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
0 ignored issues
show
Unused Code introduced by
$functionCall is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
3164
                    } else {    // did we somehow push a non-function on the stack? this should never happen
3165
                        return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack');
3166
                    }
3167
                    //    Check the argument count
3168
                    $argumentCountError = false;
3169
                    if (is_numeric($expectedArgumentCount)) {
3170
                        if ($expectedArgumentCount < 0) {
3171
                            if ($argumentCount > abs($expectedArgumentCount)) {
3172
                                $argumentCountError = true;
3173
                                $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
3174
                            }
3175
                        } else {
3176
                            if ($argumentCount != $expectedArgumentCount) {
3177
                                $argumentCountError = true;
3178
                                $expectedArgumentCountString = $expectedArgumentCount;
3179
                            }
3180
                        }
3181
                    } elseif ($expectedArgumentCount != '*') {
3182
                        $isOperandOrFunction = preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch);
0 ignored issues
show
Unused Code introduced by
$isOperandOrFunction is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
3183
                        switch ($argMatch[2]) {
3184
                            case '+':
3185
                                if ($argumentCount < $argMatch[1]) {
3186
                                    $argumentCountError = true;
3187
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
3188
                                }
3189
3190
                                break;
3191
                            case '-':
3192
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
3193
                                    $argumentCountError = true;
3194
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
3195
                                }
3196
3197
                                break;
3198
                            case ',':
3199
                                if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
3200
                                    $argumentCountError = true;
3201
                                    $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
3202
                                }
3203
3204
                                break;
3205
                        }
3206
                    }
3207
                    if ($argumentCountError) {
3208
                        return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected');
0 ignored issues
show
Bug introduced by
The variable $expectedArgumentCountString does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3209
                    }
3210
                }
3211
                ++$index;
3212
            } elseif ($opCharacter == ',') {            //    Is this the separator for function arguments?
3213
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3214
                    if ($o2 === null) {
3215
                        return $this->raiseFormulaError('Formula Error: Unexpected ,');
3216
                    }
3217
                    $output[] = $o2; // pop the argument expression stuff and push onto the output
3218
                }
3219
                //    If we've a comma when we're expecting an operand, then what we actually have is a null operand;
3220
                //        so push a null onto the stack
3221
                if (($expectingOperand) || (!$expectingOperator)) {
3222
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3223
                }
3224
                // make sure there was a function
3225
                $d = $stack->last(2);
3226
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {
3227
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
3228
                }
3229
                $d = $stack->pop();
3230
                $stack->push($d['type'], ++$d['value'], $d['reference']); // increment the argument count
3231
                $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again
3232
                $expectingOperator = false;
3233
                $expectingOperand = true;
3234
                ++$index;
3235
            } elseif ($opCharacter == '(' && !$expectingOperator) {
3236
                $stack->push('Brace', '(');
3237
                ++$index;
3238
            } elseif ($isOperandOrFunction && !$expectingOperator) {    // do we now have a function/variable/number?
3239
                $expectingOperator = true;
3240
                $expectingOperand = false;
3241
                $val = $match[1];
3242
                $length = strlen($val);
3243
3244
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
3245
                    $val = preg_replace('/\s/u', '', $val);
3246
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
3247
                        $stack->push('Function', strtoupper($val));
3248
                        $ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
3249
                        if ($ax) {
3250
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 0);
3251
                            $expectingOperator = true;
3252
                        } else {
3253
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 1);
3254
                            $expectingOperator = false;
3255
                        }
3256
                        $stack->push('Brace', '(');
3257
                    } else {    // it's a var w/ implicit multiplication
3258
                        $output[] = ['type' => 'Value', 'value' => $matches[1], 'reference' => null];
3259
                    }
3260
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
3261
                    //    Watch for this case-change when modifying to allow cell references in different worksheets...
3262
                    //    Should only be applied to the actual cell column, not the worksheet name
3263
3264
                    //    If the last entry on the stack was a : operator, then we have a cell range reference
3265
                    $testPrevOp = $stack->last(1);
3266
                    if ($testPrevOp['value'] == ':') {
3267
                        //    If we have a worksheet reference, then we're playing with a 3D reference
3268
                        if ($matches[2] == '') {
3269
                            //    Otherwise, we 'inherit' the worksheet reference from the start cell reference
3270
                            //    The start of the cell range reference should be the last entry in $output
3271
                            $startCellRef = $output[count($output) - 1]['value'];
3272
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $startCellRef, $startMatches);
3273
                            if ($startMatches[2] > '') {
3274
                                $val = $startMatches[2] . '!' . $val;
3275
                            }
3276
                        } else {
3277
                            return $this->raiseFormulaError('3D Range references are not yet supported');
3278
                        }
3279
                    }
3280
3281
                    $output[] = ['type' => 'Cell Reference', 'value' => $val, 'reference' => $val];
3282
                } else {    // it's a variable, constant, string, number or boolean
3283
                    //    If the last entry on the stack was a : operator, then we may have a row or column range reference
3284
                    $testPrevOp = $stack->last(1);
3285
                    if ($testPrevOp['value'] == ':') {
3286
                        $startRowColRef = $output[count($output) - 1]['value'];
3287
                        $rangeWS1 = '';
3288
                        if (strpos('!', $startRowColRef) !== false) {
3289
                            [$rangeWS1, $startRowColRef] = explode('!', $startRowColRef);
3290
                        }
3291
                        if ($rangeWS1 != '') {
3292
                            $rangeWS1 .= '!';
3293
                        }
3294
                        $rangeWS2 = $rangeWS1;
3295
                        if (strpos('!', $val) !== false) {
3296
                            [$rangeWS2, $val] = explode('!', $val);
3297
                        }
3298
                        if ($rangeWS2 != '') {
3299
                            $rangeWS2 .= '!';
3300
                        }
3301
                        if ((is_int($startRowColRef)) && (ctype_digit($val)) &&
3302
                            ($startRowColRef <= 1048576) && ($val <= 1048576)) {
3303
                            //    Row range
3304
                            $endRowColRef = ($pCellParent !== null) ? $pCellParent->getHighestColumn() : 'XFD'; //    Max 16,384 columns for Excel2007
3305
                            $output[count($output) - 1]['value'] = $rangeWS1 . 'A' . $startRowColRef;
3306
                            $val = $rangeWS2 . $endRowColRef . $val;
3307
                        } elseif ((ctype_alpha($startRowColRef)) && (ctype_alpha($val)) &&
3308
                            (strlen($startRowColRef) <= 3) && (strlen($val) <= 3)) {
3309
                            //    Column range
3310
                            $endRowColRef = ($pCellParent !== null) ? $pCellParent->getHighestRow() : 1048576; //    Max 1,048,576 rows for Excel2007
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3311
                            $output[count($output) - 1]['value'] = $rangeWS1 . strtoupper($startRowColRef) . '1';
3312
                            $val = $rangeWS2 . $val . $endRowColRef;
3313
                        }
3314
                    }
3315
3316
                    $localeConstant = false;
3317
                    if ($opCharacter == '"') {
3318
                        //    UnEscape any quotes within the string
3319
                        $val = self::wrapResult(str_replace('""', '"', self::unwrapResult($val)));
3320
                    } elseif (is_numeric($val)) {
3321
                        if ((strpos($val, '.') !== false) || (stripos($val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
3322
                            $val = (float) $val;
3323
                        } else {
3324
                            $val = (int) $val;
3325
                        }
3326
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
3327
                        $excelConstant = trim(strtoupper($val));
3328
                        $val = self::$excelConstants[$excelConstant];
3329
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
3330
                        $val = self::$excelConstants[$localeConstant];
3331
                    }
3332
                    $details = ['type' => 'Value', 'value' => $val, 'reference' => null];
3333
                    if ($localeConstant) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $localeConstant of type integer|false is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
3334
                        $details['localeValue'] = $localeConstant;
3335
                    }
3336
                    $output[] = $details;
3337
                }
3338
                $index += $length;
3339
            } elseif ($opCharacter == '$') {    // absolute row or column range
3340
                ++$index;
3341
            } elseif ($opCharacter == ')') {    // miscellaneous error checking
3342
                if ($expectingOperand) {
3343
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3344
                    $expectingOperand = false;
3345
                    $expectingOperator = true;
3346
                } else {
3347
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
3348
                }
3349
            } elseif (isset(self::$operators[$opCharacter]) && !$expectingOperator) {
3350
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
3351
            } else {    // I don't even want to know what you did to get here
3352
                return $this->raiseFormulaError('Formula Error: An unexpected error occured');
3353
            }
3354
            //    Test for end of formula string
3355
            if ($index == strlen($formula)) {
3356
                //    Did we end with an operator?.
3357
                //    Only valid for the % unary operator
3358
                if ((isset(self::$operators[$opCharacter])) && ($opCharacter != '%')) {
3359
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
3360
                }
3361
3362
                break;
3363
            }
3364
            //    Ignore white space
3365
            while (($formula[$index] == "\n") || ($formula[$index] == "\r")) {
3366
                ++$index;
3367
            }
3368
            if ($formula[$index] == ' ') {
3369
                while ($formula[$index] == ' ') {
3370
                    ++$index;
3371
                }
3372
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
3373
                //        Cell References) then we have an INTERSECTION operator
3374
                if (($expectingOperator) && (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
3375
                    ($output[count($output) - 1]['type'] == 'Cell Reference')) {
3376
                    while ($stack->count() > 0 &&
3377
                        ($o2 = $stack->last()) &&
3378
                        isset(self::$operators[$o2['value']]) &&
3379
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3380
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3381
                    }
3382
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
3383
                    $expectingOperator = false;
3384
                }
3385
            }
3386
        }
3387
3388
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
3389
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
3390
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
3391
            }
3392
            $output[] = $op;
3393
        }
3394
3395
        return $output;
3396
    }
3397
3398
    private static function dataTestReference(&$operandData)
3399
    {
3400
        $operand = $operandData['value'];
3401
        if (($operandData['reference'] === null) && (is_array($operand))) {
3402
            $rKeys = array_keys($operand);
3403
            $rowKey = array_shift($rKeys);
3404
            $cKeys = array_keys(array_keys($operand[$rowKey]));
3405
            $colKey = array_shift($cKeys);
3406
            if (ctype_upper($colKey)) {
3407
                $operandData['reference'] = $colKey . $rowKey;
3408
            }
3409
        }
3410
3411
        return $operand;
3412
    }
3413
3414
    // evaluate postfix notation
3415
3416
    /**
3417
     * @param mixed $tokens
3418
     * @param null|string $cellID
3419
     * @param null|Cell $pCell
3420
     *
3421
     * @return bool
3422
     */
3423
    private function processTokenStack($tokens, $cellID = null, Cell $pCell = null)
3424
    {
3425
        if ($tokens == false) {
3426
            return false;
3427
        }
3428
3429
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
3430
        //        so we store the parent cell collection so that we can re-attach it when necessary
3431
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
3432
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
3433
        $stack = new Stack();
3434
3435
        //    Loop through each token in turn
3436
        foreach ($tokens as $tokenData) {
3437
            $token = $tokenData['value'];
3438
            // 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
3439
            if (isset(self::$binaryOperators[$token])) {
3440
                //    We must have two operands, error if we don't
3441
                if (($operand2Data = $stack->pop()) === null) {
3442
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3443
                }
3444
                if (($operand1Data = $stack->pop()) === null) {
3445
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3446
                }
3447
3448
                $operand1 = self::dataTestReference($operand1Data);
3449
                $operand2 = self::dataTestReference($operand2Data);
3450
3451
                //    Log what we're doing
3452
                if ($token == ':') {
3453
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
3454
                } else {
3455
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
3456
                }
3457
3458
                //    Process the operation in the appropriate manner
3459
                switch ($token) {
3460
                    //    Comparison (Boolean) Operators
3461
                    case '>':            //    Greater than
3462
                    case '<':            //    Less than
3463
                    case '>=':            //    Greater than or Equal to
3464
                    case '<=':            //    Less than or Equal to
3465
                    case '=':            //    Equality
3466
                    case '<>':            //    Inequality
3467
                        $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
3468
3469
                        break;
3470
                    //    Binary Operators
3471
                    case ':':            //    Range
3472
                        $sheet1 = $sheet2 = '';
3473
                        if (strpos($operand1Data['reference'], '!') !== false) {
3474
                            [$sheet1, $operand1Data['reference']] = explode('!', $operand1Data['reference']);
3475
                        } else {
3476
                            $sheet1 = ($pCellParent !== null) ? $pCellWorksheet->getTitle() : '';
3477
                        }
3478
                        if (strpos($operand2Data['reference'], '!') !== false) {
3479
                            [$sheet2, $operand2Data['reference']] = explode('!', $operand2Data['reference']);
3480
                        } else {
3481
                            $sheet2 = $sheet1;
3482
                        }
3483
                        if ($sheet1 == $sheet2) {
3484
                            if ($operand1Data['reference'] === null) {
3485
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
3486
                                    $operand1Data['reference'] = $pCell->getColumn() . $operand1Data['value'];
0 ignored issues
show
Bug introduced by
It seems like $pCell is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
3487
                                } elseif (trim($operand1Data['reference']) == '') {
3488
                                    $operand1Data['reference'] = $pCell->getCoordinate();
3489
                                } else {
3490
                                    $operand1Data['reference'] = $operand1Data['value'] . $pCell->getRow();
3491
                                }
3492
                            }
3493
                            if ($operand2Data['reference'] === null) {
3494
                                if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
3495
                                    $operand2Data['reference'] = $pCell->getColumn() . $operand2Data['value'];
3496
                                } elseif (trim($operand2Data['reference']) == '') {
3497
                                    $operand2Data['reference'] = $pCell->getCoordinate();
3498
                                } else {
3499
                                    $operand2Data['reference'] = $operand2Data['value'] . $pCell->getRow();
3500
                                }
3501
                            }
3502
3503
                            $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
3504
                            $oCol = $oRow = [];
3505
                            foreach ($oData as $oDatum) {
3506
                                $oCR = Cell::coordinateFromString($oDatum);
3507
                                $oCol[] = Cell::columnIndexFromString($oCR[0]) - 1;
3508
                                $oRow[] = $oCR[1];
3509
                            }
3510
                            $cellRef = Cell::stringFromColumnIndex(min($oCol)) . min($oRow) . ':' . Cell::stringFromColumnIndex(max($oCol)) . max($oRow);
3511
                            if ($pCellParent !== null) {
3512
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
3513
                            } else {
3514
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3515
                            }
3516
                            $stack->push('Cell Reference', $cellValue, $cellRef);
3517
                        } else {
3518
                            $stack->push('Error', Calculation\Functions::REF(), null);
3519
                        }
3520
3521
                        break;
3522
                    case '+':            //    Addition
3523
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'plusEquals', $stack);
3524
3525
                        break;
3526
                    case '-':            //    Subtraction
3527
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'minusEquals', $stack);
3528
3529
                        break;
3530
                    case '*':            //    Multiplication
3531
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'arrayTimesEquals', $stack);
3532
3533
                        break;
3534
                    case '/':            //    Division
3535
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'arrayRightDivide', $stack);
3536
3537
                        break;
3538
                    case '^':            //    Exponential
3539
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'power', $stack);
3540
3541
                        break;
3542
                    case '&':            //    Concatenation
3543
                        //    If either of the operands is a matrix, we need to treat them both as matrices
3544
                        //        (converting the other operand to a matrix if need be); then perform the required
3545
                        //        matrix operation
3546
                        if (is_bool($operand1)) {
3547
                            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3548
                        }
3549
                        if (is_bool($operand2)) {
3550
                            $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3551
                        }
3552
                        if ((is_array($operand1)) || (is_array($operand2))) {
3553
                            //    Ensure that both operands are arrays/matrices
3554
                            self::checkMatrixOperands($operand1, $operand2, 2);
3555
3556
                            try {
3557
                                //    Convert operand 1 from a PHP array to a matrix
3558
                                $matrix = new Shared\JAMA\Matrix($operand1);
3559
                                //    Perform the required operation against the operand 1 matrix, passing in operand 2
3560
                                $matrixResult = $matrix->concat($operand2);
3561
                                $result = $matrixResult->getArray();
3562
                            } catch (Exception $ex) {
3563
                                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3564
                                $result = '#VALUE!';
3565
                            }
3566
                        } else {
3567
                            $result = '"' . str_replace('""', '"', self::unwrapResult($operand1) . self::unwrapResult($operand2)) . '"';
3568
                        }
3569
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3570
                        $stack->push('Value', $result);
3571
3572
                        break;
3573
                    case '|':            //    Intersect
3574
                        $rowIntersect = array_intersect_key($operand1, $operand2);
3575
                        $cellIntersect = $oCol = $oRow = [];
3576
                        foreach (array_keys($rowIntersect) as $row) {
3577
                            $oRow[] = $row;
3578
                            foreach ($rowIntersect[$row] as $col => $data) {
3579
                                $oCol[] = Cell::columnIndexFromString($col) - 1;
3580
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
3581
                            }
3582
                        }
3583
                        $cellRef = Cell::stringFromColumnIndex(min($oCol)) . min($oRow) . ':' . Cell::stringFromColumnIndex(max($oCol)) . max($oRow);
3584
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
3585
                        $stack->push('Value', $cellIntersect, $cellRef);
3586
3587
                        break;
3588
                }
3589
3590
                // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
3591
            } elseif (($token === '~') || ($token === '%')) {
3592
                if (($arg = $stack->pop()) === null) {
3593
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3594
                }
3595
                $arg = $arg['value'];
3596
                if ($token === '~') {
3597
                    $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg));
3598
                    $multiplier = -1;
3599
                } else {
3600
                    $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg));
3601
                    $multiplier = 0.01;
3602
                }
3603
                if (is_array($arg)) {
3604
                    self::checkMatrixOperands($arg, $multiplier, 2);
3605
3606
                    try {
3607
                        $matrix1 = new Shared\JAMA\Matrix($arg);
3608
                        $matrixResult = $matrix1->arrayTimesEquals($multiplier);
3609
                        $result = $matrixResult->getArray();
3610
                    } catch (Exception $ex) {
3611
                        $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3612
                        $result = '#VALUE!';
3613
                    }
3614
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3615
                    $stack->push('Value', $result);
3616
                } else {
3617
                    $this->executeNumericBinaryOperation($cellID, $multiplier, $arg, '*', 'arrayTimesEquals', $stack);
3618
                }
3619
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token, $matches)) {
3620
                $cellRef = null;
3621
                if (isset($matches[8])) {
3622
                    if ($pCell === null) {
3623
                        //                        We can't access the range, so return a REF error
3624
                        $cellValue = Calculation\Functions::REF();
3625
                    } else {
3626
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
3627
                        if ($matches[2] > '') {
3628
                            $matches[2] = trim($matches[2], "\"'");
3629
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
3630
                                //    It's a Reference to an external spreadsheet (not currently supported)
3631
                                return $this->raiseFormulaError('Unable to access External Workbook');
3632
                            }
3633
                            $matches[2] = trim($matches[2], "\"'");
3634
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]);
3635
                            if ($pCellParent !== null) {
3636
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3637
                            } else {
3638
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3639
                            }
3640
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3641
                        } else {
3642
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet');
3643
                            if ($pCellParent !== null) {
3644
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
3645
                            } else {
3646
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3647
                            }
3648
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
3649
                        }
3650
                    }
3651
                } else {
3652
                    if ($pCell === null) {
3653
                        //                        We can't access the cell, so return a REF error
3654
                        $cellValue = Calculation\Functions::REF();
3655
                    } else {
3656
                        $cellRef = $matches[6] . $matches[7];
3657
                        if ($matches[2] > '') {
3658
                            $matches[2] = trim($matches[2], "\"'");
3659
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
3660
                                //    It's a Reference to an external spreadsheet (not currently supported)
3661
                                return $this->raiseFormulaError('Unable to access External Workbook');
3662
                            }
3663
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]);
3664
                            if ($pCellParent !== null) {
3665
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
3666
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
3667
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3668
                                    $pCell->attach($pCellParent);
3669
                                } else {
3670
                                    $cellValue = null;
3671
                                }
3672
                            } else {
3673
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3674
                            }
3675
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3676
                        } else {
3677
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet');
3678
                            if ($pCellParent->has($cellRef)) {
3679
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
3680
                                $pCell->attach($pCellParent);
0 ignored issues
show
Bug introduced by
It seems like $pCellParent defined by $pCell !== null ? $pCell->getParent() : null on line 3432 can be null; however, PhpOffice\PhpSpreadsheet\Cell::attach() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
3681
                            } else {
3682
                                $cellValue = null;
3683
                            }
3684
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
3685
                        }
3686
                    }
3687
                }
3688
                $stack->push('Value', $cellValue, $cellRef);
3689
3690
                // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
3691
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) {
3692
                $functionName = $matches[1];
3693
                $argCount = $stack->pop();
3694
                $argCount = $argCount['value'];
3695
                if ($functionName != 'MKMATRIX') {
3696
                    $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's'));
3697
                }
3698
                if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) {    // function
3699
                    if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3700
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3701
                        $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
3702
                        $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
3703
                    } elseif (isset(self::$controlFunctions[$functionName])) {
3704
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
3705
                        $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
3706
                        $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
3707
                    }
3708
                    // get the arguments for this function
3709
                    $args = $argArrayVals = [];
3710
                    for ($i = 0; $i < $argCount; ++$i) {
3711
                        $arg = $stack->pop();
3712
                        $a = $argCount - $i - 1;
3713
                        if (($passByReference) &&
0 ignored issues
show
Bug introduced by
The variable $passByReference does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3714
                            (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) &&
3715
                            (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) {
3716
                            if ($arg['reference'] === null) {
3717
                                $args[] = $cellID;
3718
                                if ($functionName != 'MKMATRIX') {
3719
                                    $argArrayVals[] = $this->showValue($cellID);
3720
                                }
3721
                            } else {
3722
                                $args[] = $arg['reference'];
3723
                                if ($functionName != 'MKMATRIX') {
3724
                                    $argArrayVals[] = $this->showValue($arg['reference']);
3725
                                }
3726
                            }
3727
                        } else {
3728
                            $args[] = self::unwrapResult($arg['value']);
3729
                            if ($functionName != 'MKMATRIX') {
3730
                                $argArrayVals[] = $this->showValue($arg['value']);
3731
                            }
3732
                        }
3733
                    }
3734
                    //    Reverse the order of the arguments
3735
                    krsort($args);
3736
3737
                    if (($passByReference) && ($argCount == 0)) {
3738
                        $args[] = $cellID;
3739
                        $argArrayVals[] = $this->showValue($cellID);
3740
                    }
3741
3742
                    if ($functionName != 'MKMATRIX') {
3743
                        if ($this->debugLog->getWriteDebugLog()) {
3744
                            krsort($argArrayVals);
3745
                            $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Calculation\Functions::flattenArray($argArrayVals)), ' )');
3746
                        }
3747
                    }
3748
3749
                    //    Process the argument with the appropriate function call
3750
                    if ($passCellReference) {
0 ignored issues
show
Bug introduced by
The variable $passCellReference does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3751
                        $args[] = $pCell;
3752
                    }
3753
3754
                    if (!is_array($functionCall)) {
0 ignored issues
show
Bug introduced by
The variable $functionCall does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3755
                        foreach ($args as &$arg) {
3756
                            $arg = Calculation\Functions::flattenSingleValue($arg);
3757
                        }
3758
                        unset($arg);
3759
                    }
3760
                    $result = call_user_func_array($functionCall, $args);
3761
3762
                    if ($functionName != 'MKMATRIX') {
3763
                        $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
3764
                    }
3765
                    $stack->push('Value', self::wrapResult($result));
3766
                }
3767
            } else {
3768
                // if the token is a number, boolean, string or an Excel error, push it onto the stack
3769
                if (isset(self::$excelConstants[strtoupper($token)])) {
3770
                    $excelConstant = strtoupper($token);
3771
                    $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
3772
                    $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant]));
3773
                } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == '"') || ($token[0] == '#')) {
3774
                    $stack->push('Value', $token);
3775
                    // if the token is a named range, push the named range name onto the stack
3776
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) {
3777
                    $namedRange = $matches[6];
3778
                    $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
3779
3780
                    if (substr($namedRange, 0, 6) === '_xlfn.') {
3781
                        return $this->raiseFormulaError("undefined named range / function '$token'");
3782
                    }
3783
3784
                    $cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
3785
                    $pCell->attach($pCellParent);
0 ignored issues
show
Bug introduced by
It seems like $pCellParent defined by $pCell !== null ? $pCell->getParent() : null on line 3432 can be null; however, PhpOffice\PhpSpreadsheet\Cell::attach() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
3786
                    $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
3787
                    $stack->push('Named Range', $cellValue, $namedRange);
3788
                } else {
3789
                    return $this->raiseFormulaError("undefined variable '$token'");
3790
                }
3791
            }
3792
        }
3793
        // when we're out of tokens, the stack should have a single element, the final result
3794
        if ($stack->count() != 1) {
3795
            return $this->raiseFormulaError('internal error');
3796
        }
3797
        $output = $stack->pop();
3798
        $output = $output['value'];
3799
3800
        return $output;
3801
    }
3802
3803
    private function validateBinaryOperand($cellID, &$operand, &$stack)
0 ignored issues
show
Unused Code introduced by
The parameter $cellID is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
3804
    {
3805
        if (is_array($operand)) {
3806
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
3807
                do {
3808
                    $operand = array_pop($operand);
3809
                } while (is_array($operand));
3810
            }
3811
        }
3812
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
3813
        if (is_string($operand)) {
3814
            //    We only need special validations for the operand if it is a string
3815
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
3816
            if ($operand > '' && $operand[0] == '"') {
3817
                $operand = self::unwrapResult($operand);
3818
            }
3819
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
3820
            if (!is_numeric($operand)) {
3821
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
3822
                if ($operand > '' && $operand[0] == '#') {
3823
                    $stack->push('Value', $operand);
3824
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand));
3825
3826
                    return false;
3827
                } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
3828
                    //    If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
3829
                    $stack->push('Value', '#VALUE!');
3830
                    $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!'));
3831
3832
                    return false;
3833
                }
3834
            }
3835
        }
3836
3837
        //    return a true if the value of the operand is one that we can use in normal binary operations
3838
        return true;
3839
    }
3840
3841
    /**
3842
     * @param null|string $cellID
3843
     * @param mixed $operand1
3844
     * @param mixed $operand2
3845
     * @param string $operation
3846
     * @param Stack $stack
3847
     * @param bool $recursingArrays
3848
     *
3849
     * @return bool
3850
     */
3851
    private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
3852
    {
3853
        //    If we're dealing with matrix operations, we want a matrix result
3854
        if ((is_array($operand1)) || (is_array($operand2))) {
3855
            $result = [];
3856
            if ((is_array($operand1)) && (!is_array($operand2))) {
3857
                foreach ($operand1 as $x => $operandData) {
3858
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2));
3859
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack);
3860
                    $r = $stack->pop();
3861
                    $result[$x] = $r['value'];
3862
                }
3863
            } elseif ((!is_array($operand1)) && (is_array($operand2))) {
3864
                foreach ($operand2 as $x => $operandData) {
3865
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData));
3866
                    $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack);
3867
                    $r = $stack->pop();
3868
                    $result[$x] = $r['value'];
3869
                }
3870
            } else {
3871
                if (!$recursingArrays) {
3872
                    self::checkMatrixOperands($operand1, $operand2, 2);
3873
                }
3874
                foreach ($operand1 as $x => $operandData) {
3875
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x]));
3876
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true);
3877
                    $r = $stack->pop();
3878
                    $result[$x] = $r['value'];
3879
                }
3880
            }
3881
            //    Log the result details
3882
            $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result));
3883
            //    And push the result onto the stack
3884
            $stack->push('Array', $result);
3885
3886
            return true;
3887
        }
3888
3889
        //    Simple validate the two operands if they are string values
3890
        if (is_string($operand1) && $operand1 > '' && $operand1[0] == '"') {
3891
            $operand1 = self::unwrapResult($operand1);
3892
        }
3893
        if (is_string($operand2) && $operand2 > '' && $operand2[0] == '"') {
3894
            $operand2 = self::unwrapResult($operand2);
3895
        }
3896
3897
        // Use case insensitive comparaison if not OpenOffice mode
3898
        if (Calculation\Functions::getCompatibilityMode() != Calculation\Functions::COMPATIBILITY_OPENOFFICE) {
3899
            if (is_string($operand1)) {
3900
                $operand1 = strtoupper($operand1);
3901
            }
3902
            if (is_string($operand2)) {
3903
                $operand2 = strtoupper($operand2);
3904
            }
3905
        }
3906
3907
        $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Calculation\Functions::getCompatibilityMode() == Calculation\Functions::COMPATIBILITY_OPENOFFICE;
3908
3909
        //    execute the necessary operation
3910
        switch ($operation) {
3911
            //    Greater than
3912
            case '>':
3913
                if ($useLowercaseFirstComparison) {
3914
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0;
3915
                } else {
3916
                    $result = ($operand1 > $operand2);
3917
                }
3918
3919
                break;
3920
            //    Less than
3921
            case '<':
3922
                if ($useLowercaseFirstComparison) {
3923
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0;
3924
                } else {
3925
                    $result = ($operand1 < $operand2);
3926
                }
3927
3928
                break;
3929
            //    Equality
3930
            case '=':
3931
                if (is_numeric($operand1) && is_numeric($operand2)) {
3932
                    $result = (abs($operand1 - $operand2) < $this->delta);
3933
                } else {
3934
                    $result = strcmp($operand1, $operand2) == 0;
3935
                }
3936
3937
                break;
3938
            //    Greater than or equal
3939
            case '>=':
3940
                if (is_numeric($operand1) && is_numeric($operand2)) {
3941
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2));
3942
                } elseif ($useLowercaseFirstComparison) {
3943
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0;
3944
                } else {
3945
                    $result = strcmp($operand1, $operand2) >= 0;
3946
                }
3947
3948
                break;
3949
            //    Less than or equal
3950
            case '<=':
3951
                if (is_numeric($operand1) && is_numeric($operand2)) {
3952
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2));
3953
                } elseif ($useLowercaseFirstComparison) {
3954
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0;
3955
                } else {
3956
                    $result = strcmp($operand1, $operand2) <= 0;
3957
                }
3958
3959
                break;
3960
            //    Inequality
3961
            case '<>':
3962
                if (is_numeric($operand1) && is_numeric($operand2)) {
3963
                    $result = (abs($operand1 - $operand2) > 1E-14);
3964
                } else {
3965
                    $result = strcmp($operand1, $operand2) != 0;
3966
                }
3967
3968
                break;
3969
        }
3970
3971
        //    Log the result details
3972
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3973
        //    And push the result onto the stack
3974
        $stack->push('Value', $result);
3975
3976
        return true;
3977
    }
3978
3979
    /**
3980
     * Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
3981
     *
3982
     * @param string $str1 First string value for the comparison
3983
     * @param string $str2 Second string value for the comparison
3984
     *
3985
     * @return int
3986
     */
3987
    private function strcmpLowercaseFirst($str1, $str2)
3988
    {
3989
        $inversedStr1 = Shared\StringHelper::strCaseReverse($str1);
3990
        $inversedStr2 = Shared\StringHelper::strCaseReverse($str2);
3991
3992
        return strcmp($inversedStr1, $inversedStr2);
3993
    }
3994
3995
    /**
3996
     * @param string $matrixFunction
3997
     * @param null|string $cellID
3998
     * @param mixed $operand1
3999
     * @param mixed $operand2
4000
     * @param mixed $operation
4001
     */
4002
    private function executeNumericBinaryOperation($cellID, $operand1, $operand2, $operation, $matrixFunction, &$stack)
4003
    {
4004
        //    Validate the two operands
4005
        if (!$this->validateBinaryOperand($cellID, $operand1, $stack)) {
4006
            return false;
4007
        }
4008
        if (!$this->validateBinaryOperand($cellID, $operand2, $stack)) {
4009
            return false;
4010
        }
4011
4012
        //    If either of the operands is a matrix, we need to treat them both as matrices
4013
        //        (converting the other operand to a matrix if need be); then perform the required
4014
        //        matrix operation
4015
        if ((is_array($operand1)) || (is_array($operand2))) {
4016
            //    Ensure that both operands are arrays/matrices of the same size
4017
            self::checkMatrixOperands($operand1, $operand2, 2);
4018
4019
            try {
4020
                //    Convert operand 1 from a PHP array to a matrix
4021
                $matrix = new Shared\JAMA\Matrix($operand1);
4022
                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4023
                $matrixResult = $matrix->$matrixFunction($operand2);
4024
                $result = $matrixResult->getArray();
4025
            } catch (Exception $ex) {
4026
                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4027
                $result = '#VALUE!';
4028
            }
4029
        } else {
4030
            if ((Calculation\Functions::getCompatibilityMode() != Calculation\Functions::COMPATIBILITY_OPENOFFICE) &&
4031
                ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) ||
4032
                 (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0))) {
4033
                $result = Calculation\Functions::VALUE();
4034
            } else {
4035
                //    If we're dealing with non-matrix operations, execute the necessary operation
4036
                switch ($operation) {
4037
                    //    Addition
4038
                    case '+':
4039
                        $result = $operand1 + $operand2;
4040
4041
                        break;
4042
                    //    Subtraction
4043
                    case '-':
4044
                        $result = $operand1 - $operand2;
4045
4046
                        break;
4047
                    //    Multiplication
4048
                    case '*':
4049
                        $result = $operand1 * $operand2;
4050
4051
                        break;
4052
                    //    Division
4053
                    case '/':
4054
                        if ($operand2 == 0) {
4055
                            //    Trap for Divide by Zero error
4056
                            $stack->push('Value', '#DIV/0!');
4057
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails('#DIV/0!'));
4058
4059
                            return false;
4060
                        }
4061
                            $result = $operand1 / $operand2;
4062
4063
                        break;
4064
                    //    Power
4065
                    case '^':
4066
                        $result = pow($operand1, $operand2);
4067
4068
                        break;
4069
                }
4070
            }
4071
        }
4072
4073
        //    Log the result details
4074
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
4075
        //    And push the result onto the stack
4076
        $stack->push('Value', $result);
4077
4078
        return true;
4079
    }
4080
4081
    // trigger an error, but nicely, if need be
4082
    protected function raiseFormulaError($errorMessage)
4083
    {
4084
        $this->formulaError = $errorMessage;
4085
        $this->cyclicReferenceStack->clear();
0 ignored issues
show
Bug introduced by
The method clear cannot be called on $this->cyclicReferenceStack (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
4086
        if (!$this->suppressFormulaErrors) {
4087
            throw new Calculation\Exception($errorMessage);
4088
        }
4089
        trigger_error($errorMessage, E_USER_ERROR);
4090
    }
4091
4092
    /**
4093
     * Extract range values.
4094
     *
4095
     * @param string &$pRange String based range representation
4096
     * @param Worksheet $pSheet Worksheet
4097
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4098
     *
4099
     * @throws Calculation\Exception
4100
     *
4101
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4102
     */
4103
    public function extractCellRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4104
    {
4105
        // Return value
4106
        $returnValue = [];
4107
4108
        if ($pSheet !== null) {
4109
            $pSheetName = $pSheet->getTitle();
4110
            if (strpos($pRange, '!') !== false) {
4111
                [$pSheetName, $pRange] = Worksheet::extractSheetTitle($pRange, true);
4112
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4113
            }
4114
4115
            // Extract range
4116
            $aReferences = Cell::extractAllCellReferencesInRange($pRange);
4117
            $pRange = $pSheetName . '!' . $pRange;
4118
            if (!isset($aReferences[1])) {
4119
                //    Single cell in range
4120
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
0 ignored issues
show
Bug introduced by
The variable $currentRow does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
4121
                if ($pSheet->cellExists($aReferences[0])) {
4122
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4123
                } else {
4124
                    $returnValue[$currentRow][$currentCol] = null;
4125
                }
4126
            } else {
4127
                // Extract cell data for all cells in the range
4128
                foreach ($aReferences as $reference) {
4129
                    // Extract range
4130
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
4131
                    if ($pSheet->cellExists($reference)) {
4132
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4133
                    } else {
4134
                        $returnValue[$currentRow][$currentCol] = null;
4135
                    }
4136
                }
4137
            }
4138
        }
4139
4140
        return $returnValue;
4141
    }
4142
4143
    /**
4144
     * Extract range values.
4145
     *
4146
     * @param string &$pRange String based range representation
4147
     * @param Worksheet $pSheet Worksheet
4148
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4149
     *
4150
     * @throws Calculation\Exception
4151
     *
4152
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4153
     */
4154
    public function extractNamedRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4155
    {
4156
        // Return value
4157
        $returnValue = [];
4158
4159
        if ($pSheet !== null) {
4160
            $pSheetName = $pSheet->getTitle();
4161
            if (strpos($pRange, '!') !== false) {
4162
                [$pSheetName, $pRange] = Worksheet::extractSheetTitle($pRange, true);
4163
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4164
            }
4165
4166
            // Named range?
4167
            $namedRange = NamedRange::resolveRange($pRange, $pSheet);
4168
            if ($namedRange !== null) {
4169
                $pSheet = $namedRange->getWorksheet();
4170
                $pRange = $namedRange->getRange();
4171
                $splitRange = Cell::splitRange($pRange);
4172
                //    Convert row and column references
4173
                if (ctype_alpha($splitRange[0][0])) {
4174
                    $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
4175
                } elseif (ctype_digit($splitRange[0][0])) {
4176
                    $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
4177
                }
4178
            } else {
4179
                return Calculation\Functions::REF();
4180
            }
4181
4182
            // Extract range
4183
            $aReferences = Cell::extractAllCellReferencesInRange($pRange);
4184
            if (!isset($aReferences[1])) {
4185
                //    Single cell (or single column or row) in range
4186
                [$currentCol, $currentRow] = Cell::coordinateFromString($aReferences[0]);
0 ignored issues
show
Bug introduced by
The variable $currentCol does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $currentRow does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
4187
                if ($pSheet->cellExists($aReferences[0])) {
4188
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4189
                } else {
4190
                    $returnValue[$currentRow][$currentCol] = null;
4191
                }
4192
            } else {
4193
                // Extract cell data for all cells in the range
4194
                foreach ($aReferences as $reference) {
4195
                    // Extract range
4196
                    [$currentCol, $currentRow] = Cell::coordinateFromString($reference);
4197
                    if ($pSheet->cellExists($reference)) {
4198
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4199
                    } else {
4200
                        $returnValue[$currentRow][$currentCol] = null;
4201
                    }
4202
                }
4203
            }
4204
        }
4205
4206
        return $returnValue;
4207
    }
4208
4209
    /**
4210
     * Is a specific function implemented?
4211
     *
4212
     * @param string $pFunction Function Name
4213
     *
4214
     * @return bool
4215
     */
4216
    public function isImplemented($pFunction)
4217
    {
4218
        $pFunction = strtoupper($pFunction);
4219
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
4220
4221
        return !$notImplemented;
4222
    }
4223
4224
    /**
4225
     * Get a list of all implemented functions as an array of function objects.
4226
     *
4227
     * @return array of Calculation\Category
4228
     */
4229
    public function getFunctions()
4230
    {
4231
        return self::$phpSpreadsheetFunctions;
4232
    }
4233
4234
    /**
4235
     * Get a list of implemented Excel function names.
4236
     *
4237
     * @return array
4238
     */
4239
    public function getImplementedFunctionNames()
4240
    {
4241
        $returnValue = [];
4242
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
4243
            if ($this->isImplemented($functionName)) {
4244
                $returnValue[] = $functionName;
4245
            }
4246
        }
4247
4248
        return $returnValue;
4249
    }
4250
}
4251