Completed
Push — develop ( 4635d3...bf2dbb )
by Adrien
27:59
created

Calculation::translateFormula()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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

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

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

Loading history...
1999 157
        $this->debugLog = new Logger($this->cyclicReferenceStack);
2000 157
    }
2001
2002 1
    private static function loadLocales()
2003
    {
2004 1
        $localeFileDirectory = __DIR__ . '/locale/';
2005 1
        foreach (glob($localeFileDirectory . '/*', GLOB_ONLYDIR) as $filename) {
2006 1
            $filename = substr($filename, strlen($localeFileDirectory) + 1);
2007 1
            if ($filename != 'en') {
2008 1
                self::$validLocaleLanguages[] = $filename;
2009
            }
2010
        }
2011 1
    }
2012
2013
    /**
2014
     * Get an instance of this class.
2015
     *
2016
     * @param Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object,
2017
     *                                    or NULL to create a standalone claculation engine
2018
     *
2019
     * @return Calculation
2020
     */
2021 534
    public static function getInstance(Spreadsheet $spreadsheet = null)
2022
    {
2023 534
        if ($spreadsheet !== null) {
2024 103
            $instance = $spreadsheet->getCalculationEngine();
2025 103
            if (isset($instance)) {
2026 103
                return $instance;
2027
            }
2028
        }
2029
2030 445
        if (!isset(self::$instance) || (self::$instance === null)) {
2031 14
            self::$instance = new self();
2032
        }
2033
2034 445
        return self::$instance;
2035
    }
2036
2037
    /**
2038
     * Flush the calculation cache for any existing instance of this class
2039
     *        but only if a Calculation instance exists.
2040
     */
2041
    public function flushInstance()
2042
    {
2043
        $this->clearCalculationCache();
2044
    }
2045
2046
    /**
2047
     * Get the Logger for this calculation engine instance.
2048
     *
2049
     * @return Logger
2050
     */
2051 74
    public function getDebugLog()
2052
    {
2053 74
        return $this->debugLog;
2054
    }
2055
2056
    /**
2057
     * __clone implementation. Cloning should not be allowed in a Singleton!
2058
     *
2059
     * @throws Exception
2060
     */
2061
    final public function __clone()
2062
    {
2063
        throw new Exception('Cloning the calculation engine is not allowed!');
2064
    }
2065
2066
    /**
2067
     * Return the locale-specific translation of TRUE.
2068
     *
2069
     * @return string locale-specific translation of TRUE
2070
     */
2071 37
    public static function getTRUE()
2072
    {
2073 37
        return self::$localeBoolean['TRUE'];
2074
    }
2075
2076
    /**
2077
     * Return the locale-specific translation of FALSE.
2078
     *
2079
     * @return string locale-specific translation of FALSE
2080
     */
2081 30
    public static function getFALSE()
2082
    {
2083 30
        return self::$localeBoolean['FALSE'];
2084
    }
2085
2086
    /**
2087
     * Set the Array Return Type (Array or Value of first element in the array).
2088
     *
2089
     * @param string $returnType Array return type
2090
     *
2091
     * @return bool Success or failure
2092
     */
2093 26
    public static function setArrayReturnType($returnType)
2094
    {
2095 26
        if (($returnType == self::RETURN_ARRAY_AS_VALUE) ||
2096 19
            ($returnType == self::RETURN_ARRAY_AS_ERROR) ||
2097 26
            ($returnType == self::RETURN_ARRAY_AS_ARRAY)) {
2098 26
            self::$returnArrayAsType = $returnType;
2099
2100 26
            return true;
2101
        }
2102
2103
        return false;
2104
    }
2105
2106
    /**
2107
     * Return the Array Return Type (Array or Value of first element in the array).
2108
     *
2109
     * @return string $returnType Array return type
2110
     */
2111 15
    public static function getArrayReturnType()
2112
    {
2113 15
        return self::$returnArrayAsType;
2114
    }
2115
2116
    /**
2117
     * Is calculation caching enabled?
2118
     *
2119
     * @return bool
2120
     */
2121 1
    public function getCalculationCacheEnabled()
2122
    {
2123 1
        return $this->calculationCacheEnabled;
2124
    }
2125
2126
    /**
2127
     * Enable/disable calculation cache.
2128
     *
2129
     * @param bool $pValue
2130
     */
2131
    public function setCalculationCacheEnabled($pValue)
2132
    {
2133
        $this->calculationCacheEnabled = $pValue;
2134
        $this->clearCalculationCache();
2135
    }
2136
2137
    /**
2138
     * Enable calculation cache.
2139
     */
2140
    public function enableCalculationCache()
2141
    {
2142
        $this->setCalculationCacheEnabled(true);
2143
    }
2144
2145
    /**
2146
     * Disable calculation cache.
2147
     */
2148
    public function disableCalculationCache()
2149
    {
2150
        $this->setCalculationCacheEnabled(false);
2151
    }
2152
2153
    /**
2154
     * Clear calculation cache.
2155
     */
2156
    public function clearCalculationCache()
2157
    {
2158
        $this->calculationCache = [];
2159
    }
2160
2161
    /**
2162
     * Clear calculation cache for a specified worksheet.
2163
     *
2164
     * @param string $worksheetName
2165
     */
2166 3
    public function clearCalculationCacheForWorksheet($worksheetName)
2167
    {
2168 3
        if (isset($this->calculationCache[$worksheetName])) {
2169
            unset($this->calculationCache[$worksheetName]);
2170
        }
2171 3
    }
2172
2173
    /**
2174
     * Rename calculation cache for a specified worksheet.
2175
     *
2176
     * @param string $fromWorksheetName
2177
     * @param string $toWorksheetName
2178
     */
2179 157
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName)
2180
    {
2181 157
        if (isset($this->calculationCache[$fromWorksheetName])) {
2182
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2183
            unset($this->calculationCache[$fromWorksheetName]);
2184
        }
2185 157
    }
2186
2187
    /**
2188
     * Get the currently defined locale code.
2189
     *
2190
     * @return string
2191
     */
2192
    public function getLocale()
2193
    {
2194
        return self::$localeLanguage;
2195
    }
2196
2197
    /**
2198
     * Set the locale code.
2199
     *
2200
     * @param string $locale The locale to use for formula translation, eg: 'en_us'
2201
     *
2202
     * @return bool
2203
     */
2204 426
    public function setLocale($locale)
2205
    {
2206
        //    Identify our locale and language
2207 426
        $language = $locale = strtolower($locale);
2208 426
        if (strpos($locale, '_') !== false) {
2209 426
            list($language) = explode('_', $locale);
2210
        }
2211
2212 426
        if (count(self::$validLocaleLanguages) == 1) {
2213 1
            self::loadLocales();
2214
        }
2215
        //    Test whether we have any language data for this language (any locale)
2216 426
        if (in_array($language, self::$validLocaleLanguages)) {
2217
            //    initialise language/locale settings
2218 426
            self::$localeFunctions = [];
2219 426
            self::$localeArgumentSeparator = ',';
2220 426
            self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL'];
2221
            //    Default is English, if user isn't requesting english, then read the necessary data from the locale files
2222 426
            if ($locale != 'en_us') {
2223
                //    Search for a file with a list of function names for locale
2224 17
                $functionNamesFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'functions';
2225 17
                if (!file_exists($functionNamesFile)) {
2226
                    //    If there isn't a locale specific function file, look for a language specific function file
2227
                    $functionNamesFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'functions';
2228
                    if (!file_exists($functionNamesFile)) {
2229
                        return false;
2230
                    }
2231
                }
2232
                //    Retrieve the list of locale or language specific function names
2233 17
                $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2234 17
                foreach ($localeFunctions as $localeFunction) {
2235 17
                    list($localeFunction) = explode('##', $localeFunction); //    Strip out comments
2236 17
                    if (strpos($localeFunction, '=') !== false) {
2237 17
                        list($fName, $lfName) = explode('=', $localeFunction);
2238 17
                        $fName = trim($fName);
2239 17
                        $lfName = trim($lfName);
2240 17
                        if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) {
2241 17
                            self::$localeFunctions[$fName] = $lfName;
2242
                        }
2243
                    }
2244
                }
2245
                //    Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions
2246 17
                if (isset(self::$localeFunctions['TRUE'])) {
2247 17
                    self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE'];
2248
                }
2249 17
                if (isset(self::$localeFunctions['FALSE'])) {
2250 17
                    self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE'];
2251
                }
2252
2253 17
                $configFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'config';
2254 17
                if (!file_exists($configFile)) {
2255
                    $configFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'config';
2256
                }
2257 17
                if (file_exists($configFile)) {
2258 17
                    $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2259 17
                    foreach ($localeSettings as $localeSetting) {
2260 17
                        list($localeSetting) = explode('##', $localeSetting); //    Strip out comments
2261 17
                        if (strpos($localeSetting, '=') !== false) {
2262 17
                            list($settingName, $settingValue) = explode('=', $localeSetting);
2263 17
                            $settingName = strtoupper(trim($settingName));
2264
                            switch ($settingName) {
2265 17
                                case 'ARGUMENTSEPARATOR':
2266 17
                                    self::$localeArgumentSeparator = trim($settingValue);
2267
2268 17
                                    break;
2269
                            }
2270
                        }
2271
                    }
2272
                }
2273
            }
2274
2275 426
            self::$functionReplaceFromExcel = self::$functionReplaceToExcel =
2276 426
            self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
2277 426
            self::$localeLanguage = $locale;
2278
2279 426
            return true;
2280
        }
2281
2282
        return false;
2283
    }
2284
2285
    /**
2286
     * @param string $fromSeparator
2287
     * @param string $toSeparator
2288
     * @param string $formula
2289
     * @param bool $inBraces
2290
     *
2291
     * @return string
2292
     */
2293 5
    public static function translateSeparator($fromSeparator, $toSeparator, $formula, &$inBraces)
2294
    {
2295 5
        $strlen = mb_strlen($formula);
2296 5
        for ($i = 0; $i < $strlen; ++$i) {
2297 5
            $chr = mb_substr($formula, $i, 1);
2298
            switch ($chr) {
2299 5
                case '{':
2300
                    $inBraces = true;
2301
2302
                    break;
2303 5
                case '}':
2304
                    $inBraces = false;
2305
2306
                    break;
2307 5
                case $fromSeparator:
2308
                    if (!$inBraces) {
2309
                        $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
2310
                    }
2311
            }
2312
        }
2313
2314 5
        return $formula;
2315
    }
2316
2317
    /**
2318
     * @param string $fromSeparator
2319
     * @param string $toSeparator
2320
     * @param mixed $from
2321
     * @param mixed $to
2322
     * @param mixed $formula
2323
     */
2324
    private static function translateFormula($from, $to, $formula, $fromSeparator, $toSeparator)
2325
    {
2326
        //    Convert any Excel function names to the required language
2327
        if (self::$localeLanguage !== 'en_us') {
2328
            $inBraces = false;
2329
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
2330
            if (strpos($formula, '"') !== false) {
2331
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
2332
                //        the formula
2333
                $temp = explode('"', $formula);
2334
                $i = false;
2335
                foreach ($temp as &$value) {
2336
                    //    Only count/replace in alternating array entries
2337
                    if ($i = !$i) {
2338
                        $value = preg_replace($from, $to, $value);
2339
                        $value = self::translateSeparator($fromSeparator, $toSeparator, $value, $inBraces);
2340
                    }
2341
                }
2342
                unset($value);
2343
                //    Then rebuild the formula string
2344
                $formula = implode('"', $temp);
2345
            } else {
2346
                //    If there's no quoted strings, then we do a simple count/replace
2347
                $formula = preg_replace($from, $to, $formula);
2348
                $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inBraces);
2349
            }
2350
        }
2351
2352
        return $formula;
2353
    }
2354
2355
    private static $functionReplaceFromExcel = null;
2356
    private static $functionReplaceToLocale = null;
2357
2358
    public function _translateFormulaToLocale($formula)
2359
    {
2360
        if (self::$functionReplaceFromExcel === null) {
2361
            self::$functionReplaceFromExcel = [];
2362
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2363
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/Ui';
2364
            }
2365
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2366
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui';
2367
            }
2368
        }
2369
2370
        if (self::$functionReplaceToLocale === null) {
2371
            self::$functionReplaceToLocale = [];
2372
            foreach (array_values(self::$localeFunctions) as $localeFunctionName) {
2373
                self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2';
2374
            }
2375
            foreach (array_values(self::$localeBoolean) as $localeBoolean) {
2376
                self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2';
2377
            }
2378
        }
2379
2380
        return self::translateFormula(self::$functionReplaceFromExcel, self::$functionReplaceToLocale, $formula, ',', self::$localeArgumentSeparator);
2381
    }
2382
2383
    private static $functionReplaceFromLocale = null;
2384
    private static $functionReplaceToExcel = null;
2385
2386
    public function _translateFormulaToEnglish($formula)
2387
    {
2388
        if (self::$functionReplaceFromLocale === null) {
2389
            self::$functionReplaceFromLocale = [];
2390
            foreach (array_values(self::$localeFunctions) as $localeFunctionName) {
2391
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/Ui';
2392
            }
2393
            foreach (array_values(self::$localeBoolean) as $excelBoolean) {
2394
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui';
2395
            }
2396
        }
2397
2398
        if (self::$functionReplaceToExcel === null) {
2399
            self::$functionReplaceToExcel = [];
2400
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2401
                self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2';
2402
            }
2403
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2404
                self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2';
2405
            }
2406
        }
2407
2408
        return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ',');
2409
    }
2410
2411 46
    public static function localeFunc($function)
2412
    {
2413 46
        if (self::$localeLanguage !== 'en_us') {
2414
            $functionName = trim($function, '(');
2415
            if (isset(self::$localeFunctions[$functionName])) {
2416
                $brace = ($functionName != $function);
2417
                $function = self::$localeFunctions[$functionName];
2418
                if ($brace) {
2419
                    $function .= '(';
2420
                }
2421
            }
2422
        }
2423
2424 46
        return $function;
2425
    }
2426
2427
    /**
2428
     * Wrap string values in quotes.
2429
     *
2430
     * @param mixed $value
2431
     *
2432
     * @return mixed
2433
     */
2434 80
    public static function wrapResult($value)
2435
    {
2436 80
        if (is_string($value)) {
2437
            //    Error values cannot be "wrapped"
2438 68
            if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
2439
                //    Return Excel errors "as is"
2440 10
                return $value;
2441
            }
2442
            //    Return strings wrapped in quotes
2443 59
            return '"' . $value . '"';
2444
            //    Convert numeric errors to NaN error
2445 44
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2446
            return Functions::NAN();
2447
        }
2448
2449 44
        return $value;
2450
    }
2451
2452
    /**
2453
     * Remove quotes used as a wrapper to identify string values.
2454
     *
2455
     * @param mixed $value
2456
     *
2457
     * @return mixed
2458
     */
2459 94
    public static function unwrapResult($value)
2460
    {
2461 94
        if (is_string($value)) {
2462 73
            if ((isset($value[0])) && ($value[0] == '"') && (substr($value, -1) == '"')) {
2463 73
                return substr($value, 1, -1);
2464
            }
2465
            //    Convert numeric errors to NAN error
2466 59
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2467
            return Functions::NAN();
2468
        }
2469
2470 60
        return $value;
2471
    }
2472
2473
    /**
2474
     * Calculate cell value (using formula from a cell ID)
2475
     * Retained for backward compatibility.
2476
     *
2477
     * @param Cell $pCell Cell to calculate
2478
     *
2479
     * @throws Exception
2480
     *
2481
     * @return mixed
2482
     */
2483
    public function calculate(Cell $pCell = null)
2484
    {
2485
        try {
2486
            return $this->calculateCellValue($pCell);
2487
        } catch (\Exception $e) {
2488
            throw new Exception($e->getMessage());
2489
        }
2490
    }
2491
2492
    /**
2493
     * Calculate the value of a cell formula.
2494
     *
2495
     * @param Cell $pCell Cell to calculate
2496
     * @param bool $resetLog Flag indicating whether the debug log should be reset or not
2497
     *
2498
     * @throws Exception
2499
     *
2500
     * @return mixed
2501
     */
2502 48
    public function calculateCellValue(Cell $pCell = null, $resetLog = true)
2503
    {
2504 48
        if ($pCell === null) {
2505
            return null;
2506
        }
2507
2508 48
        $returnArrayAsType = self::$returnArrayAsType;
2509 48
        if ($resetLog) {
2510
            //    Initialise the logging settings if requested
2511 47
            $this->formulaError = null;
2512 47
            $this->debugLog->clearLog();
2513 47
            $this->cyclicReferenceStack->clear();
2514 47
            $this->cyclicFormulaCounter = 1;
2515
2516 47
            self::$returnArrayAsType = self::RETURN_ARRAY_AS_ARRAY;
2517
        }
2518
2519
        //    Execute the calculation for the cell formula
2520 48
        $this->cellStack[] = [
2521 48
            'sheet' => $pCell->getWorksheet()->getTitle(),
2522 48
            'cell' => $pCell->getCoordinate(),
2523
        ];
2524
2525
        try {
2526 48
            $result = self::unwrapResult($this->_calculateFormulaValue($pCell->getValue(), $pCell->getCoordinate(), $pCell));
2527 48
            $cellAddress = array_pop($this->cellStack);
2528 48
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2529
        } catch (\Exception $e) {
2530
            $cellAddress = array_pop($this->cellStack);
2531
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2532
2533
            throw new Exception($e->getMessage());
2534
        }
2535
2536 48
        if ((is_array($result)) && (self::$returnArrayAsType != self::RETURN_ARRAY_AS_ARRAY)) {
2537 2
            self::$returnArrayAsType = $returnArrayAsType;
2538 2
            $testResult = Functions::flattenArray($result);
2539 2
            if (self::$returnArrayAsType == self::RETURN_ARRAY_AS_ERROR) {
2540
                return Functions::VALUE();
2541
            }
2542
            //    If there's only a single cell in the array, then we allow it
2543 2
            if (count($testResult) != 1) {
2544
                //    If keys are numeric, then it's a matrix result rather than a cell range result, so we permit it
2545
                $r = array_keys($result);
2546
                $r = array_shift($r);
2547
                if (!is_numeric($r)) {
2548
                    return Functions::VALUE();
2549
                }
2550
                if (is_array($result[$r])) {
2551
                    $c = array_keys($result[$r]);
2552
                    $c = array_shift($c);
2553
                    if (!is_numeric($c)) {
2554
                        return Functions::VALUE();
2555
                    }
2556
                }
2557
            }
2558 2
            $result = array_shift($testResult);
2559
        }
2560 48
        self::$returnArrayAsType = $returnArrayAsType;
2561
2562 48
        if ($result === null) {
2563
            return 0;
2564 48
        } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
2565
            return Functions::NAN();
2566
        }
2567
2568 48
        return $result;
2569
    }
2570
2571
    /**
2572
     * Validate and parse a formula string.
2573
     *
2574
     * @param string $formula Formula to parse
2575
     *
2576
     * @throws Exception
2577
     *
2578
     * @return array
2579
     */
2580
    public function parseFormula($formula)
2581
    {
2582
        //    Basic validation that this is indeed a formula
2583
        //    We return an empty array if not
2584
        $formula = trim($formula);
2585
        if ((!isset($formula[0])) || ($formula[0] != '=')) {
2586
            return [];
2587
        }
2588
        $formula = ltrim(substr($formula, 1));
2589
        if (!isset($formula[0])) {
2590
            return [];
2591
        }
2592
2593
        //    Parse the formula and return the token stack
2594
        return $this->_parseFormula($formula);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_parseFormula($formula) returns the type boolean which is incompatible with the documented return type array.
Loading history...
2595
    }
2596
2597
    /**
2598
     * Calculate the value of a formula.
2599
     *
2600
     * @param string $formula Formula to parse
2601
     * @param string $cellID Address of the cell to calculate
2602
     * @param Cell $pCell Cell to calculate
2603
     *
2604
     * @throws Exception
2605
     *
2606
     * @return mixed
2607
     */
2608 1
    public function calculateFormula($formula, $cellID = null, Cell $pCell = null)
2609
    {
2610
        //    Initialise the logging settings
2611 1
        $this->formulaError = null;
2612 1
        $this->debugLog->clearLog();
2613 1
        $this->cyclicReferenceStack->clear();
2614
2615 1
        if ($this->spreadsheet !== null && $cellID === null && $pCell === null) {
2616
            $cellID = 'A1';
2617
            $pCell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
2618
        } else {
2619
            //    Disable calculation cacheing because it only applies to cell calculations, not straight formulae
2620
            //    But don't actually flush any cache
2621 1
            $resetCache = $this->getCalculationCacheEnabled();
2622 1
            $this->calculationCacheEnabled = false;
2623
        }
2624
2625
        //    Execute the calculation
2626
        try {
2627 1
            $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $pCell));
2628 1
        } catch (\Exception $e) {
2629 1
            throw new Exception($e->getMessage());
2630
        }
2631
2632 1
        if ($this->spreadsheet === null) {
2633
            //    Reset calculation cacheing to its previous state
2634
            $this->calculationCacheEnabled = $resetCache;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $resetCache does not seem to be defined for all execution paths leading up to this point.
Loading history...
2635
        }
2636
2637 1
        return $result;
2638
    }
2639
2640
    /**
2641
     * @param string $cellReference
2642
     * @param mixed $cellValue
2643
     *
2644
     * @return bool
2645
     */
2646 49
    public function getValueFromCache($cellReference, &$cellValue)
2647
    {
2648
        // Is calculation cacheing enabled?
2649
        // Is the value present in calculation cache?
2650 49
        $this->debugLog->writeDebugLog('Testing cache value for cell ', $cellReference);
2651 49
        if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
2652 40
            $this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache');
2653
            // Return the cached result
2654 40
            $cellValue = $this->calculationCache[$cellReference];
2655
2656 40
            return true;
2657
        }
2658
2659 49
        return false;
2660
    }
2661
2662
    /**
2663
     * @param string $cellReference
2664
     * @param mixed $cellValue
2665
     */
2666 49
    public function saveValueToCache($cellReference, $cellValue)
2667
    {
2668 49
        if ($this->calculationCacheEnabled) {
2669 48
            $this->calculationCache[$cellReference] = $cellValue;
2670
        }
2671 49
    }
2672
2673
    /**
2674
     * Parse a cell formula and calculate its value.
2675
     *
2676
     * @param string $formula The formula to parse and calculate
2677
     * @param string $cellID The ID (e.g. A3) of the cell that we are calculating
2678
     * @param Cell $pCell Cell to calculate
2679
     *
2680
     * @throws Exception
2681
     *
2682
     * @return mixed
2683
     */
2684 118
    public function _calculateFormulaValue($formula, $cellID = null, Cell $pCell = null)
2685
    {
2686 118
        $cellValue = null;
2687
2688
        //    Basic validation that this is indeed a formula
2689
        //    We simply return the cell value if not
2690 118
        $formula = trim($formula);
2691 118
        if ($formula[0] != '=') {
2692
            return self::wrapResult($formula);
2693
        }
2694 118
        $formula = ltrim(substr($formula, 1));
2695 118
        if (!isset($formula[0])) {
2696
            return self::wrapResult($formula);
2697
        }
2698
2699 118
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
2700 118
        $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
2701 118
        $wsCellReference = $wsTitle . '!' . $cellID;
2702
2703 118
        if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
2704 40
            return $cellValue;
2705
        }
2706
2707 118
        if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) {
2708
            if ($this->cyclicFormulaCount <= 0) {
2709
                $this->cyclicFormulaCell = '';
2710
2711
                return $this->raiseFormulaError('Cyclic Reference in Formula');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError... Reference in Formula') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2712
            } elseif ($this->cyclicFormulaCell === $wsCellReference) {
2713
                ++$this->cyclicFormulaCounter;
2714
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2715
                    $this->cyclicFormulaCell = '';
2716
2717
                    return $cellValue;
2718
                }
2719
            } elseif ($this->cyclicFormulaCell == '') {
2720
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2721
                    return $cellValue;
2722
                }
2723
                $this->cyclicFormulaCell = $wsCellReference;
2724
            }
2725
        }
2726
2727
        //    Parse the formula onto the token stack and calculate the value
2728 118
        $this->cyclicReferenceStack->push($wsCellReference);
2729 118
        $cellValue = $this->processTokenStack($this->_parseFormula($formula, $pCell), $cellID, $pCell);
2730 118
        $this->cyclicReferenceStack->pop();
2731
2732
        // Save to calculation cache
2733 118
        if ($cellID !== null) {
2734 49
            $this->saveValueToCache($wsCellReference, $cellValue);
2735
        }
2736
2737
        //    Return the calculated value
2738 118
        return $cellValue;
2739
    }
2740
2741
    /**
2742
     * Ensure that paired matrix operands are both matrices and of the same size.
2743
     *
2744
     * @param mixed &$operand1 First matrix operand
2745
     * @param mixed &$operand2 Second matrix operand
2746
     * @param int $resize Flag indicating whether the matrices should be resized to match
2747
     *                                        and (if so), whether the smaller dimension should grow or the
2748
     *                                        larger should shrink.
2749
     *                                            0 = no resize
2750
     *                                            1 = shrink to fit
2751
     *                                            2 = extend to fit
2752
     */
2753 5
    private static function checkMatrixOperands(&$operand1, &$operand2, $resize = 1)
2754
    {
2755
        //    Examine each of the two operands, and turn them into an array if they aren't one already
2756
        //    Note that this function should only be called if one or both of the operand is already an array
2757 5
        if (!is_array($operand1)) {
2758
            list($matrixRows, $matrixColumns) = self::getMatrixDimensions($operand2);
2759
            $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1));
2760
            $resize = 0;
2761 5
        } elseif (!is_array($operand2)) {
2762 2
            list($matrixRows, $matrixColumns) = self::getMatrixDimensions($operand1);
2763 2
            $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2));
2764 2
            $resize = 0;
2765
        }
2766
2767 5
        list($matrix1Rows, $matrix1Columns) = self::getMatrixDimensions($operand1);
2768 5
        list($matrix2Rows, $matrix2Columns) = self::getMatrixDimensions($operand2);
2769 5
        if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
2770 5
            $resize = 1;
2771
        }
2772
2773 5
        if ($resize == 2) {
2774
            //    Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger
2775
            self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2776 5
        } elseif ($resize == 1) {
2777
            //    Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller
2778 5
            self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2779
        }
2780
2781 5
        return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns];
2782
    }
2783
2784
    /**
2785
     * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0.
2786
     *
2787
     * @param array &$matrix matrix operand
2788
     *
2789
     * @return int[] An array comprising the number of rows, and number of columns
2790
     */
2791 15
    public static function getMatrixDimensions(array &$matrix)
2792
    {
2793 15
        $matrixRows = count($matrix);
2794 15
        $matrixColumns = 0;
2795 15
        foreach ($matrix as $rowKey => $rowValue) {
2796 13
            $matrixColumns = max(count($rowValue), $matrixColumns);
2797 13
            if (!is_array($rowValue)) {
2798 4
                $matrix[$rowKey] = [$rowValue];
2799
            } else {
2800 13
                $matrix[$rowKey] = array_values($rowValue);
2801
            }
2802
        }
2803 15
        $matrix = array_values($matrix);
2804
2805 15
        return [$matrixRows, $matrixColumns];
2806
    }
2807
2808
    /**
2809
     * Ensure that paired matrix operands are both matrices of the same size.
2810
     *
2811
     * @param mixed &$matrix1 First matrix operand
2812
     * @param mixed &$matrix2 Second matrix operand
2813
     * @param int $matrix1Rows Row size of first matrix operand
2814
     * @param int $matrix1Columns Column size of first matrix operand
2815
     * @param int $matrix2Rows Row size of second matrix operand
2816
     * @param int $matrix2Columns Column size of second matrix operand
2817
     */
2818 5
    private static function resizeMatricesShrink(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
2819
    {
2820 5
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
2821
            if ($matrix2Rows < $matrix1Rows) {
2822
                for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) {
2823
                    unset($matrix1[$i]);
2824
                }
2825
            }
2826
            if ($matrix2Columns < $matrix1Columns) {
2827
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2828
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
2829
                        unset($matrix1[$i][$j]);
2830
                    }
2831
                }
2832
            }
2833
        }
2834
2835 5
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
2836
            if ($matrix1Rows < $matrix2Rows) {
2837
                for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) {
2838
                    unset($matrix2[$i]);
2839
                }
2840
            }
2841
            if ($matrix1Columns < $matrix2Columns) {
2842
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2843
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
2844
                        unset($matrix2[$i][$j]);
2845
                    }
2846
                }
2847
            }
2848
        }
2849 5
    }
2850
2851
    /**
2852
     * Ensure that paired matrix operands are both matrices of the same size.
2853
     *
2854
     * @param mixed &$matrix1 First matrix operand
2855
     * @param mixed &$matrix2 Second matrix operand
2856
     * @param int $matrix1Rows Row size of first matrix operand
2857
     * @param int $matrix1Columns Column size of first matrix operand
2858
     * @param int $matrix2Rows Row size of second matrix operand
2859
     * @param int $matrix2Columns Column size of second matrix operand
2860
     */
2861
    private static function resizeMatricesExtend(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
2862
    {
2863
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
2864
            if ($matrix2Columns < $matrix1Columns) {
2865
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2866
                    $x = $matrix2[$i][$matrix2Columns - 1];
2867
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
2868
                        $matrix2[$i][$j] = $x;
2869
                    }
2870
                }
2871
            }
2872
            if ($matrix2Rows < $matrix1Rows) {
2873
                $x = $matrix2[$matrix2Rows - 1];
2874
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2875
                    $matrix2[$i] = $x;
2876
                }
2877
            }
2878
        }
2879
2880
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
2881
            if ($matrix1Columns < $matrix2Columns) {
2882
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2883
                    $x = $matrix1[$i][$matrix1Columns - 1];
2884
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
2885
                        $matrix1[$i][$j] = $x;
2886
                    }
2887
                }
2888
            }
2889
            if ($matrix1Rows < $matrix2Rows) {
2890
                $x = $matrix1[$matrix1Rows - 1];
2891
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2892
                    $matrix1[$i] = $x;
2893
                }
2894
            }
2895
        }
2896
    }
2897
2898
    /**
2899
     * Format details of an operand for display in the log (based on operand type).
2900
     *
2901
     * @param mixed $value First matrix operand
2902
     *
2903
     * @return mixed
2904
     */
2905 117
    private function showValue($value)
2906
    {
2907 117
        if ($this->debugLog->getWriteDebugLog()) {
2908
            $testArray = Functions::flattenArray($value);
2909
            if (count($testArray) == 1) {
2910
                $value = array_pop($testArray);
2911
            }
2912
2913
            if (is_array($value)) {
2914
                $returnMatrix = [];
2915
                $pad = $rpad = ', ';
2916
                foreach ($value as $row) {
2917
                    if (is_array($row)) {
2918
                        $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
2919
                        $rpad = '; ';
2920
                    } else {
2921
                        $returnMatrix[] = $this->showValue($row);
2922
                    }
2923
                }
2924
2925
                return '{ ' . implode($rpad, $returnMatrix) . ' }';
2926
            } elseif (is_string($value) && (trim($value, '"') == $value)) {
2927
                return '"' . $value . '"';
2928
            } elseif (is_bool($value)) {
2929
                return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
2930
            }
2931
        }
2932
2933 117
        return Functions::flattenSingleValue($value);
2934
    }
2935
2936
    /**
2937
     * Format type and details of an operand for display in the log (based on operand type).
2938
     *
2939
     * @param mixed $value First matrix operand
2940
     *
2941
     * @return null|string
2942
     */
2943 117
    private function showTypeDetails($value)
2944
    {
2945 117
        if ($this->debugLog->getWriteDebugLog()) {
2946
            $testArray = Functions::flattenArray($value);
2947
            if (count($testArray) == 1) {
2948
                $value = array_pop($testArray);
2949
            }
2950
2951
            if ($value === null) {
2952
                return 'a NULL value';
2953
            } elseif (is_float($value)) {
2954
                $typeString = 'a floating point number';
2955
            } elseif (is_int($value)) {
2956
                $typeString = 'an integer number';
2957
            } elseif (is_bool($value)) {
2958
                $typeString = 'a boolean';
2959
            } elseif (is_array($value)) {
2960
                $typeString = 'a matrix';
2961
            } else {
2962
                if ($value == '') {
2963
                    return 'an empty string';
2964
                } elseif ($value[0] == '#') {
2965
                    return 'a ' . $value . ' error';
2966
                }
2967
                $typeString = 'a string';
2968
            }
2969
2970
            return $typeString . ' with a value of ' . $this->showValue($value);
2971
        }
2972 117
    }
2973
2974
    /**
2975
     * @param string $formula
2976
     *
2977
     * @return string
2978
     */
2979 118
    private function convertMatrixReferences($formula)
2980
    {
2981 118
        static $matrixReplaceFrom = ['{', ';', '}'];
2982 118
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
2983
2984
        //    Convert any Excel matrix references to the MKMATRIX() function
2985 118
        if (strpos($formula, '{') !== false) {
2986
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
2987
            if (strpos($formula, '"') !== false) {
2988
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
2989
                //        the formula
2990
                $temp = explode('"', $formula);
2991
                //    Open and Closed counts used for trapping mismatched braces in the formula
2992
                $openCount = $closeCount = 0;
2993
                $i = false;
2994
                foreach ($temp as &$value) {
2995
                    //    Only count/replace in alternating array entries
2996
                    if ($i = !$i) {
2997
                        $openCount += substr_count($value, '{');
2998
                        $closeCount += substr_count($value, '}');
2999
                        $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
3000
                    }
3001
                }
3002
                unset($value);
3003
                //    Then rebuild the formula string
3004
                $formula = implode('"', $temp);
3005
            } else {
3006
                //    If there's no quoted strings, then we do a simple count/replace
3007
                $openCount = substr_count($formula, '{');
3008
                $closeCount = substr_count($formula, '}');
3009
                $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
3010
            }
3011
            //    Trap for mismatched braces and trigger an appropriate error
3012
            if ($openCount < $closeCount) {
3013
                if ($openCount > 0) {
3014
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...hed matrix braces '}'') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3015
                }
3016
3017
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...ected '}' encountered') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3018
            } elseif ($openCount > $closeCount) {
3019
                if ($closeCount > 0) {
3020
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...hed matrix braces '{'') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3021
                }
3022
3023
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...ected '{' encountered') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3024
            }
3025
        }
3026
3027 118
        return $formula;
3028
    }
3029
3030
    private static function mkMatrix(...$args)
3031
    {
3032
        return $args;
3033
    }
3034
3035
    //    Binary Operators
3036
    //    These operators always work on two values
3037
    //    Array key is the operator, the value indicates whether this is a left or right associative operator
3038
    private static $operatorAssociativity = [
3039
        '^' => 0, //    Exponentiation
3040
        '*' => 0, '/' => 0, //    Multiplication and Division
3041
        '+' => 0, '-' => 0, //    Addition and Subtraction
3042
        '&' => 0, //    Concatenation
3043
        '|' => 0, ':' => 0, //    Intersect and Range
3044
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3045
    ];
3046
3047
    //    Comparison (Boolean) Operators
3048
    //    These operators work on two values, but always return a boolean result
3049
    private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
3050
3051
    //    Operator Precedence
3052
    //    This list includes all valid operators, whether binary (including boolean) or unary (such as %)
3053
    //    Array key is the operator, the value is its precedence
3054
    private static $operatorPrecedence = [
3055
        ':' => 8, //    Range
3056
        '|' => 7, //    Intersect
3057
        '~' => 6, //    Negation
3058
        '%' => 5, //    Percentage
3059
        '^' => 4, //    Exponentiation
3060
        '*' => 3, '/' => 3, //    Multiplication and Division
3061
        '+' => 2, '-' => 2, //    Addition and Subtraction
3062
        '&' => 1, //    Concatenation
3063
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3064
    ];
3065
3066
    // Convert infix to postfix notation
3067
3068
    /**
3069
     * @param string $formula
3070
     * @param null|\PhpOffice\PhpSpreadsheet\Cell\Cell $pCell
3071
     *
3072
     * @return bool
3073
     */
3074 118
    private function _parseFormula($formula, Cell $pCell = null)
3075
    {
3076 118
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
3077
            return false;
3078
        }
3079
3080
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
3081
        //        so we store the parent worksheet so that we can re-attach it when necessary
3082 118
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3083
3084 118
        $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION .
3085 118
                                '|' . self::CALCULATION_REGEXP_CELLREF .
3086 118
                                '|' . self::CALCULATION_REGEXP_NUMBER .
3087 118
                                '|' . self::CALCULATION_REGEXP_STRING .
3088 118
                                '|' . self::CALCULATION_REGEXP_OPENBRACE .
3089 118
                                '|' . self::CALCULATION_REGEXP_NAMEDRANGE .
3090 118
                                '|' . self::CALCULATION_REGEXP_ERROR .
3091 118
                                ')/si';
3092
3093
        //    Start with initialisation
3094 118
        $index = 0;
3095 118
        $stack = new Stack();
3096 118
        $output = [];
3097 118
        $expectingOperator = false; //    We use this test in syntax-checking the expression to determine when a
3098
                                                    //        - is a negation or + is a positive operator rather than an operation
3099 118
        $expectingOperand = false; //    We use this test in syntax-checking the expression to determine whether an operand
3100
                                                    //        should be null in a function call
3101
        //    The guts of the lexical parser
3102
        //    Loop through the formula extracting each operator and operand in turn
3103 118
        while (true) {
3104 118
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
3105 118
            if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
3106 34
                $opCharacter .= $formula[++$index];
3107
            }
3108
3109
            //    Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
3110 118
            $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
3111
3112 118
            if ($opCharacter == '-' && !$expectingOperator) {                //    Is it a negation instead of a minus?
3113 2
                $stack->push('Unary Operator', '~'); //    Put a negation on the stack
3114 2
                ++$index; //        and drop the negation symbol
3115 118
            } elseif ($opCharacter == '%' && $expectingOperator) {
3116
                $stack->push('Unary Operator', '%'); //    Put a percentage on the stack
3117
                ++$index;
3118 118
            } elseif ($opCharacter == '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
3119
                ++$index; //    Drop the redundant plus symbol
3120 118
            } elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) {    //    We have to explicitly deny a tilde or pipe, because they are legal
3121
                return $this->raiseFormulaError("Formula Error: Illegal character '~'"); //        on the stack but not in the input expression
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...Illegal character '~'') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3122 118
            } 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...
3123 111
                while ($stack->count() > 0 &&
3124 111
                    ($o2 = $stack->last()) &&
3125 111
                    isset(self::$operators[$o2['value']]) &&
3126 111
                    @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3127 2
                    $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3128
                }
3129 111
                $stack->push('Binary Operator', $opCharacter); //    Finally put our current operator onto the stack
3130 111
                ++$index;
3131 111
                $expectingOperator = false;
3132 118
            } elseif ($opCharacter == ')' && $expectingOperator) {            //    Are we expecting to close a parenthesis?
3133 46
                $expectingOperand = false;
3134 46
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3135 39
                    if ($o2 === null) {
3136
                        return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...ted closing brace ")"') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3137
                    }
3138 39
                    $output[] = $o2;
3139
                }
3140 46
                $d = $stack->last(2);
3141 46
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {    //    Did this parenthesis just close a function?
3142 46
                    $functionName = $matches[1]; //    Get the function name
3143 46
                    $d = $stack->pop();
3144 46
                    $argumentCount = $d['value']; //    See how many arguments there were (argument count is the next value stored on the stack)
3145 46
                    $output[] = $d; //    Dump the argument count on the output
3146 46
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
3147 46
                    if (isset(self::$controlFunctions[$functionName])) {
3148
                        $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
3149
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
0 ignored issues
show
Unused Code introduced by
The assignment to $functionCall is dead and can be removed.
Loading history...
3150 46
                    } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3151 46
                        $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
3152 46
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3153
                    } else {    // did we somehow push a non-function on the stack? this should never happen
3154
                        return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...non-function on stack') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3155
                    }
3156
                    //    Check the argument count
3157 46
                    $argumentCountError = false;
3158 46
                    if (is_numeric($expectedArgumentCount)) {
3159 25
                        if ($expectedArgumentCount < 0) {
3160
                            if ($argumentCount > abs($expectedArgumentCount)) {
3161
                                $argumentCountError = true;
3162
                                $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
3163
                            }
3164
                        } else {
3165 25
                            if ($argumentCount != $expectedArgumentCount) {
3166
                                $argumentCountError = true;
3167 25
                                $expectedArgumentCountString = $expectedArgumentCount;
3168
                            }
3169
                        }
3170 40
                    } elseif ($expectedArgumentCount != '*') {
3171 40
                        $isOperandOrFunction = preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch);
0 ignored issues
show
Unused Code introduced by
The assignment to $isOperandOrFunction is dead and can be removed.
Loading history...
3172 40
                        switch ($argMatch[2]) {
3173 40
                            case '+':
3174 38
                                if ($argumentCount < $argMatch[1]) {
3175
                                    $argumentCountError = true;
3176
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
3177
                                }
3178
3179 38
                                break;
3180 16
                            case '-':
3181 13
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
3182
                                    $argumentCountError = true;
3183
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
3184
                                }
3185
3186 13
                                break;
3187 3
                            case ',':
3188 3
                                if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
3189
                                    $argumentCountError = true;
3190
                                    $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
3191
                                }
3192
3193 3
                                break;
3194
                        }
3195
                    }
3196 46
                    if ($argumentCountError) {
3197
                        return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $expectedArgumentCountString does not seem to be defined for all execution paths leading up to this point.
Loading history...
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...ntString . ' expected') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3198
                    }
3199
                }
3200 46
                ++$index;
3201 118
            } elseif ($opCharacter == ',') {            //    Is this the separator for function arguments?
3202 30
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3203 25
                    if ($o2 === null) {
3204
                        return $this->raiseFormulaError('Formula Error: Unexpected ,');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...a Error: Unexpected ,') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3205
                    }
3206 25
                    $output[] = $o2; // pop the argument expression stuff and push onto the output
3207
                }
3208
                //    If we've a comma when we're expecting an operand, then what we actually have is a null operand;
3209
                //        so push a null onto the stack
3210 30
                if (($expectingOperand) || (!$expectingOperator)) {
3211
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3212
                }
3213
                // make sure there was a function
3214 30
                $d = $stack->last(2);
3215 30
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {
3216
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...a Error: Unexpected ,') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3217
                }
3218 30
                $d = $stack->pop();
3219 30
                $stack->push($d['type'], ++$d['value'], $d['reference']); // increment the argument count
3220 30
                $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again
3221 30
                $expectingOperator = false;
3222 30
                $expectingOperand = true;
3223 30
                ++$index;
3224 118
            } elseif ($opCharacter == '(' && !$expectingOperator) {
3225 3
                $stack->push('Brace', '(');
3226 3
                ++$index;
3227 118
            } elseif ($isOperandOrFunction && !$expectingOperator) {    // do we now have a function/variable/number?
3228 118
                $expectingOperator = true;
3229 118
                $expectingOperand = false;
3230 118
                $val = $match[1];
3231 118
                $length = strlen($val);
3232
3233 118
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
3234 46
                    $val = preg_replace('/\s/u', '', $val);
3235 46
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
3236 46
                        $stack->push('Function', strtoupper($val));
3237 46
                        $ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
3238 46
                        if ($ax) {
3239 9
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 0);
3240 9
                            $expectingOperator = true;
3241
                        } else {
3242 46
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 1);
3243 46
                            $expectingOperator = false;
3244
                        }
3245 46
                        $stack->push('Brace', '(');
3246
                    } else {    // it's a var w/ implicit multiplication
3247 46
                        $output[] = ['type' => 'Value', 'value' => $matches[1], 'reference' => null];
3248
                    }
3249 118
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
3250
                    //    Watch for this case-change when modifying to allow cell references in different worksheets...
3251
                    //    Should only be applied to the actual cell column, not the worksheet name
3252
3253
                    //    If the last entry on the stack was a : operator, then we have a cell range reference
3254 60
                    $testPrevOp = $stack->last(1);
3255 60
                    if ($testPrevOp['value'] == ':') {
3256
                        //    If we have a worksheet reference, then we're playing with a 3D reference
3257 52
                        if ($matches[2] == '') {
3258
                            //    Otherwise, we 'inherit' the worksheet reference from the start cell reference
3259
                            //    The start of the cell range reference should be the last entry in $output
3260 52
                            $startCellRef = $output[count($output) - 1]['value'];
3261 52
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $startCellRef, $startMatches);
3262 52
                            if ($startMatches[2] > '') {
3263 52
                                $val = $startMatches[2] . '!' . $val;
3264
                            }
3265
                        } else {
3266
                            return $this->raiseFormulaError('3D Range references are not yet supported');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...are not yet supported') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3267
                        }
3268
                    }
3269
3270 60
                    $output[] = ['type' => 'Cell Reference', 'value' => $val, 'reference' => $val];
3271
                } else {    // it's a variable, constant, string, number or boolean
3272
                    //    If the last entry on the stack was a : operator, then we may have a row or column range reference
3273 95
                    $testPrevOp = $stack->last(1);
3274 95
                    if ($testPrevOp['value'] == ':') {
3275 1
                        $startRowColRef = $output[count($output) - 1]['value'];
3276 1
                        $rangeWS1 = '';
3277 1
                        if (strpos('!', $startRowColRef) !== false) {
3278
                            list($rangeWS1, $startRowColRef) = explode('!', $startRowColRef);
3279
                        }
3280 1
                        if ($rangeWS1 != '') {
3281
                            $rangeWS1 .= '!';
3282
                        }
3283 1
                        $rangeWS2 = $rangeWS1;
3284 1
                        if (strpos('!', $val) !== false) {
3285
                            list($rangeWS2, $val) = explode('!', $val);
3286
                        }
3287 1
                        if ($rangeWS2 != '') {
3288
                            $rangeWS2 .= '!';
3289
                        }
3290 1
                        if ((is_int($startRowColRef)) && (ctype_digit($val)) &&
3291 1
                            ($startRowColRef <= 1048576) && ($val <= 1048576)) {
3292
                            //    Row range
3293
                            $endRowColRef = ($pCellParent !== null) ? $pCellParent->getHighestColumn() : 'XFD'; //    Max 16,384 columns for Excel2007
3294
                            $output[count($output) - 1]['value'] = $rangeWS1 . 'A' . $startRowColRef;
3295
                            $val = $rangeWS2 . $endRowColRef . $val;
3296 1
                        } elseif ((ctype_alpha($startRowColRef)) && (ctype_alpha($val)) &&
3297 1
                            (strlen($startRowColRef) <= 3) && (strlen($val) <= 3)) {
3298
                            //    Column range
3299
                            $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...
3300
                            $output[count($output) - 1]['value'] = $rangeWS1 . strtoupper($startRowColRef) . '1';
3301
                            $val = $rangeWS2 . $val . $endRowColRef;
3302
                        }
3303
                    }
3304
3305 95
                    $localeConstant = false;
3306 95
                    if ($opCharacter == '"') {
3307
                        //    UnEscape any quotes within the string
3308 59
                        $val = self::wrapResult(str_replace('""', '"', self::unwrapResult($val)));
3309 62
                    } elseif (is_numeric($val)) {
3310 60
                        if ((strpos($val, '.') !== false) || (stripos($val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
3311 21
                            $val = (float) $val;
3312
                        } else {
3313 60
                            $val = (int) $val;
3314
                        }
3315 6
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
3316 1
                        $excelConstant = trim(strtoupper($val));
3317 1
                        $val = self::$excelConstants[$excelConstant];
3318 5
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
3319
                        $val = self::$excelConstants[$localeConstant];
3320
                    }
3321 95
                    $details = ['type' => 'Value', 'value' => $val, 'reference' => null];
3322 95
                    if ($localeConstant) {
3323
                        $details['localeValue'] = $localeConstant;
3324
                    }
3325 95
                    $output[] = $details;
3326
                }
3327 118
                $index += $length;
3328
            } elseif ($opCharacter == '$') {    // absolute row or column range
3329
                ++$index;
3330
            } elseif ($opCharacter == ')') {    // miscellaneous error checking
3331
                if ($expectingOperand) {
3332
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3333
                    $expectingOperand = false;
3334
                    $expectingOperator = true;
3335
                } else {
3336
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...Error: Unexpected ')'') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3337
                }
3338
            } elseif (isset(self::$operators[$opCharacter]) && !$expectingOperator) {
3339
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError(EncapsedNode) targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3340
            } else {    // I don't even want to know what you did to get here
3341
                return $this->raiseFormulaError('Formula Error: An unexpected error occured');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...xpected error occured') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3342
            }
3343
            //    Test for end of formula string
3344 118
            if ($index == strlen($formula)) {
3345
                //    Did we end with an operator?.
3346
                //    Only valid for the % unary operator
3347 118
                if ((isset(self::$operators[$opCharacter])) && ($opCharacter != '%')) {
3348
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError(EncapsedNode) targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3349
                }
3350
3351 118
                break;
3352
            }
3353
            //    Ignore white space
3354 117
            while (($formula[$index] == "\n") || ($formula[$index] == "\r")) {
3355
                ++$index;
3356
            }
3357 117
            if ($formula[$index] == ' ') {
3358 55
                while ($formula[$index] == ' ') {
3359 55
                    ++$index;
3360
                }
3361
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
3362
                //        Cell References) then we have an INTERSECTION operator
3363 55
                if (($expectingOperator) && (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
3364 55
                    ($output[count($output) - 1]['type'] == 'Cell Reference')) {
3365
                    while ($stack->count() > 0 &&
3366
                        ($o2 = $stack->last()) &&
3367
                        isset(self::$operators[$o2['value']]) &&
3368
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3369
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3370
                    }
3371
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
3372
                    $expectingOperator = false;
3373
                }
3374
            }
3375
        }
3376
3377 118
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
3378 98
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
3379
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError... Error: Expecting ')'') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3380
            }
3381 98
            $output[] = $op;
3382
        }
3383
3384 118
        return $output;
3385
    }
3386
3387 111
    private static function dataTestReference(&$operandData)
3388
    {
3389 111
        $operand = $operandData['value'];
3390 111
        if (($operandData['reference'] === null) && (is_array($operand))) {
3391 1
            $rKeys = array_keys($operand);
3392 1
            $rowKey = array_shift($rKeys);
3393 1
            $cKeys = array_keys(array_keys($operand[$rowKey]));
3394 1
            $colKey = array_shift($cKeys);
3395 1
            if (ctype_upper($colKey)) {
3396
                $operandData['reference'] = $colKey . $rowKey;
3397
            }
3398
        }
3399
3400 111
        return $operand;
3401
    }
3402
3403
    // evaluate postfix notation
3404
3405
    /**
3406
     * @param mixed $tokens
3407
     * @param null|string $cellID
3408
     * @param null|Cell $pCell
3409
     *
3410
     * @return bool
3411
     */
3412 118
    private function processTokenStack($tokens, $cellID = null, Cell $pCell = null)
3413
    {
3414 118
        if ($tokens == false) {
3415
            return false;
3416
        }
3417
3418
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
3419
        //        so we store the parent cell collection so that we can re-attach it when necessary
3420 118
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
3421 118
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
3422 118
        $stack = new Stack();
3423
3424
        //    Loop through each token in turn
3425 118
        foreach ($tokens as $tokenData) {
3426 118
            $token = $tokenData['value'];
3427
            // 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
3428 118
            if (isset(self::$binaryOperators[$token])) {
3429
                //    We must have two operands, error if we don't
3430 111
                if (($operand2Data = $stack->pop()) === null) {
3431
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...ue missing from stack') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3432
                }
3433 111
                if (($operand1Data = $stack->pop()) === null) {
3434
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...ue missing from stack') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3435
                }
3436
3437 111
                $operand1 = self::dataTestReference($operand1Data);
3438 111
                $operand2 = self::dataTestReference($operand2Data);
3439
3440
                //    Log what we're doing
3441 111
                if ($token == ':') {
3442 52
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
3443
                } else {
3444 94
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
3445
                }
3446
3447
                //    Process the operation in the appropriate manner
3448
                switch ($token) {
3449
                    //    Comparison (Boolean) Operators
3450 111
                    case '>':            //    Greater than
3451 97
                    case '<':            //    Less than
3452 90
                    case '>=':            //    Greater than or Equal to
3453 82
                    case '<=':            //    Less than or Equal to
3454 75
                    case '=':            //    Equality
3455 66
                    case '<>':            //    Inequality
3456 74
                        $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
3457
3458 74
                        break;
3459
                    //    Binary Operators
3460 59
                    case ':':            //    Range
3461 52
                        $sheet1 = $sheet2 = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $sheet1 is dead and can be removed.
Loading history...
3462 52
                        if (strpos($operand1Data['reference'], '!') !== false) {
3463 52
                            list($sheet1, $operand1Data['reference']) = explode('!', $operand1Data['reference']);
3464
                        } else {
3465 1
                            $sheet1 = ($pCellParent !== null) ? $pCellWorksheet->getTitle() : '';
3466
                        }
3467 52
                        if (strpos($operand2Data['reference'], '!') !== false) {
3468 52
                            list($sheet2, $operand2Data['reference']) = explode('!', $operand2Data['reference']);
3469
                        } else {
3470 10
                            $sheet2 = $sheet1;
3471
                        }
3472 52
                        if ($sheet1 == $sheet2) {
3473 52
                            if ($operand1Data['reference'] === null) {
3474
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
3475
                                    $operand1Data['reference'] = $pCell->getColumn() . $operand1Data['value'];
1 ignored issue
show
Bug introduced by
The method getColumn() does not exist on null. ( Ignorable by Annotation )

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

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

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

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

Loading history...
3476
                                } elseif (trim($operand1Data['reference']) == '') {
3477
                                    $operand1Data['reference'] = $pCell->getCoordinate();
3478
                                } else {
3479
                                    $operand1Data['reference'] = $operand1Data['value'] . $pCell->getRow();
3480
                                }
3481
                            }
3482 52
                            if ($operand2Data['reference'] === null) {
3483
                                if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
3484
                                    $operand2Data['reference'] = $pCell->getColumn() . $operand2Data['value'];
3485
                                } elseif (trim($operand2Data['reference']) == '') {
3486
                                    $operand2Data['reference'] = $pCell->getCoordinate();
3487
                                } else {
3488
                                    $operand2Data['reference'] = $operand2Data['value'] . $pCell->getRow();
3489
                                }
3490
                            }
3491
3492 52
                            $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
3493 52
                            $oCol = $oRow = [];
3494 52
                            foreach ($oData as $oDatum) {
3495 52
                                $oCR = Coordinate::coordinateFromString($oDatum);
3496 52
                                $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
3497 52
                                $oRow[] = $oCR[1];
3498
                            }
3499 52
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
3500 52
                            if ($pCellParent !== null) {
3501 52
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
3502
                            } else {
3503
                                return $this->raiseFormulaError('Unable to access Cell Reference');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...access Cell Reference') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3504
                            }
3505 52
                            $stack->push('Cell Reference', $cellValue, $cellRef);
3506
                        } else {
3507
                            $stack->push('Error', Functions::REF(), null);
3508
                        }
3509
3510 52
                        break;
3511 32
                    case '+':            //    Addition
3512 23
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
3513
3514 23
                        break;
3515 28
                    case '-':            //    Subtraction
3516 7
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
3517
3518 7
                        break;
3519 26
                    case '*':            //    Multiplication
3520 19
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
3521
3522 19
                        break;
3523 11
                    case '/':            //    Division
3524 8
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
3525
3526 8
                        break;
3527 6
                    case '^':            //    Exponential
3528
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
3529
3530
                        break;
3531 6
                    case '&':            //    Concatenation
3532
                        //    If either of the operands is a matrix, we need to treat them both as matrices
3533
                        //        (converting the other operand to a matrix if need be); then perform the required
3534
                        //        matrix operation
3535 6
                        if (is_bool($operand1)) {
3536
                            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3537
                        }
3538 6
                        if (is_bool($operand2)) {
3539
                            $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3540
                        }
3541 6
                        if ((is_array($operand1)) || (is_array($operand2))) {
3542
                            //    Ensure that both operands are arrays/matrices
3543 5
                            self::checkMatrixOperands($operand1, $operand2, 2);
3544
3545
                            try {
3546
                                //    Convert operand 1 from a PHP array to a matrix
3547 5
                                $matrix = new Shared\JAMA\Matrix($operand1);
3548
                                //    Perform the required operation against the operand 1 matrix, passing in operand 2
3549 5
                                $matrixResult = $matrix->concat($operand2);
3550 5
                                $result = $matrixResult->getArray();
3551
                            } catch (\Exception $ex) {
3552
                                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3553 5
                                $result = '#VALUE!';
3554
                            }
3555
                        } else {
3556 1
                            $result = '"' . str_replace('""', '"', self::unwrapResult($operand1) . self::unwrapResult($operand2)) . '"';
3557
                        }
3558 6
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3559 6
                        $stack->push('Value', $result);
3560
3561 6
                        break;
3562
                    case '|':            //    Intersect
3563
                        $rowIntersect = array_intersect_key($operand1, $operand2);
3564
                        $cellIntersect = $oCol = $oRow = [];
3565
                        foreach (array_keys($rowIntersect) as $row) {
3566
                            $oRow[] = $row;
3567
                            foreach ($rowIntersect[$row] as $col => $data) {
3568
                                $oCol[] = Coordinate::columnIndexFromString($col) - 1;
3569
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
3570
                            }
3571
                        }
3572
                        $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
3573
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
3574
                        $stack->push('Value', $cellIntersect, $cellRef);
3575
3576 111
                        break;
3577
                }
3578
3579
                // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
3580 118
            } elseif (($token === '~') || ($token === '%')) {
3581 2
                if (($arg = $stack->pop()) === null) {
3582
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...ue missing from stack') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3583
                }
3584 2
                $arg = $arg['value'];
3585 2
                if ($token === '~') {
3586 2
                    $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg));
3587 2
                    $multiplier = -1;
3588
                } else {
3589
                    $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg));
3590
                    $multiplier = 0.01;
3591
                }
3592 2
                if (is_array($arg)) {
3593
                    self::checkMatrixOperands($arg, $multiplier, 2);
3594
3595
                    try {
3596
                        $matrix1 = new Shared\JAMA\Matrix($arg);
3597
                        $matrixResult = $matrix1->arrayTimesEquals($multiplier);
3598
                        $result = $matrixResult->getArray();
3599
                    } catch (\Exception $ex) {
3600
                        $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3601
                        $result = '#VALUE!';
3602
                    }
3603
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3604
                    $stack->push('Value', $result);
3605
                } else {
3606 2
                    $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack);
3607
                }
3608 118
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token, $matches)) {
3609 60
                $cellRef = null;
3610 60
                if (isset($matches[8])) {
3611
                    if ($pCell === null) {
3612
                        //                        We can't access the range, so return a REF error
3613
                        $cellValue = Functions::REF();
3614
                    } else {
3615
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
3616
                        if ($matches[2] > '') {
3617
                            $matches[2] = trim($matches[2], "\"'");
3618
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
3619
                                //    It's a Reference to an external spreadsheet (not currently supported)
3620
                                return $this->raiseFormulaError('Unable to access External Workbook');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...ess External Workbook') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3621
                            }
3622
                            $matches[2] = trim($matches[2], "\"'");
3623
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]);
3624
                            if ($pCellParent !== null) {
3625
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3626
                            } else {
3627
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3628
                            }
3629
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3630
                        } else {
3631
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet');
3632
                            if ($pCellParent !== null) {
3633
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
3634
                            } else {
3635
                                return $this->raiseFormulaError('Unable to access Cell Reference');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...access Cell Reference') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3636
                            }
3637
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
3638
                        }
3639
                    }
3640
                } else {
3641 60
                    if ($pCell === null) {
3642
                        //                        We can't access the cell, so return a REF error
3643
                        $cellValue = Functions::REF();
3644
                    } else {
3645 60
                        $cellRef = $matches[6] . $matches[7];
3646 60
                        if ($matches[2] > '') {
3647 14
                            $matches[2] = trim($matches[2], "\"'");
3648 14
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
3649
                                //    It's a Reference to an external spreadsheet (not currently supported)
3650
                                return $this->raiseFormulaError('Unable to access External Workbook');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError...ess External Workbook') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3651
                            }
3652 14
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]);
3653 14
                            if ($pCellParent !== null) {
3654 14
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
3655 14
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
3656 14
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3657 14
                                    $pCell->attach($pCellParent);
3658
                                } else {
3659 14
                                    $cellValue = null;
3660
                                }
3661
                            } else {
3662
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3663
                            }
3664 14
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3665
                        } else {
3666 48
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet');
3667 48
                            if ($pCellParent->has($cellRef)) {
1 ignored issue
show
Bug introduced by
The method has() does not exist on null. ( Ignorable by Annotation )

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

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

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

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

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

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

3669
                                $pCell->attach(/** @scrutinizer ignore-type */ $pCellParent);
Loading history...
3670
                            } else {
3671 2
                                $cellValue = null;
3672
                            }
3673 48
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
3674
                        }
3675
                    }
3676
                }
3677 60
                $stack->push('Value', $cellValue, $cellRef);
3678
3679
                // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
3680 104
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) {
3681 46
                $functionName = $matches[1];
3682 46
                $argCount = $stack->pop();
3683 46
                $argCount = $argCount['value'];
3684 46
                if ($functionName != 'MKMATRIX') {
3685 46
                    $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's'));
3686
                }
3687 46
                if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) {    // function
3688 46
                    if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3689 46
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3690 46
                        $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
3691 46
                        $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
3692
                    } elseif (isset(self::$controlFunctions[$functionName])) {
3693
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
3694
                        $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
3695
                        $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
3696
                    }
3697
                    // get the arguments for this function
3698 46
                    $args = $argArrayVals = [];
3699 46
                    for ($i = 0; $i < $argCount; ++$i) {
3700 46
                        $arg = $stack->pop();
3701 46
                        $a = $argCount - $i - 1;
3702 46
                        if (($passByReference) &&
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $passByReference does not seem to be defined for all execution paths leading up to this point.
Loading history...
3703 46
                            (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) &&
3704 46
                            (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) {
3705
                            if ($arg['reference'] === null) {
3706
                                $args[] = $cellID;
3707
                                if ($functionName != 'MKMATRIX') {
3708
                                    $argArrayVals[] = $this->showValue($cellID);
3709
                                }
3710
                            } else {
3711
                                $args[] = $arg['reference'];
3712
                                if ($functionName != 'MKMATRIX') {
3713
                                    $argArrayVals[] = $this->showValue($arg['reference']);
3714
                                }
3715
                            }
3716
                        } else {
3717 46
                            $args[] = self::unwrapResult($arg['value']);
3718 46
                            if ($functionName != 'MKMATRIX') {
3719 46
                                $argArrayVals[] = $this->showValue($arg['value']);
3720
                            }
3721
                        }
3722
                    }
3723
                    //    Reverse the order of the arguments
3724 46
                    krsort($args);
3725
3726 46
                    if (($passByReference) && ($argCount == 0)) {
3727
                        $args[] = $cellID;
3728
                        $argArrayVals[] = $this->showValue($cellID);
3729
                    }
3730
3731 46
                    if ($functionName != 'MKMATRIX') {
3732 46
                        if ($this->debugLog->getWriteDebugLog()) {
3733
                            krsort($argArrayVals);
3734
                            $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )');
3735
                        }
3736
                    }
3737
3738
                    //    Process the argument with the appropriate function call
3739 46
                    if ($passCellReference) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $passCellReference does not seem to be defined for all execution paths leading up to this point.
Loading history...
3740 1
                        $args[] = $pCell;
3741
                    }
3742
3743 46
                    if (!is_array($functionCall)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $functionCall does not seem to be defined for all execution paths leading up to this point.
Loading history...
3744 1
                        foreach ($args as &$arg) {
3745
                            $arg = Functions::flattenSingleValue($arg);
3746
                        }
3747 1
                        unset($arg);
3748
                    }
3749 46
                    $result = call_user_func_array($functionCall, $args);
3750
3751 46
                    if ($functionName != 'MKMATRIX') {
3752 46
                        $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
3753
                    }
3754 46
                    $stack->push('Value', self::wrapResult($result));
3755
                }
3756
            } else {
3757
                // if the token is a number, boolean, string or an Excel error, push it onto the stack
3758 104
                if (isset(self::$excelConstants[strtoupper($token)])) {
3759
                    $excelConstant = strtoupper($token);
3760
                    $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
3761
                    $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant]));
3762 104
                } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == '"') || ($token[0] == '#')) {
3763 104
                    $stack->push('Value', $token);
3764
                    // if the token is a named range, push the named range name onto the stack
3765 5
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) {
3766 5
                    $namedRange = $matches[6];
3767 5
                    $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
3768
3769 5
                    if (substr($namedRange, 0, 6) === '_xlfn.') {
3770
                        return $this->raiseFormulaError("undefined named range / function '$token'");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError(EncapsedNode) targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3771
                    }
3772
3773 5
                    $cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
3774 5
                    $pCell->attach($pCellParent);
3775 5
                    $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
3776 5
                    $stack->push('Named Range', $cellValue, $namedRange);
3777
                } else {
3778 118
                    return $this->raiseFormulaError("undefined variable '$token'");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError(EncapsedNode) targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3779
                }
3780
            }
3781
        }
3782
        // when we're out of tokens, the stack should have a single element, the final result
3783 118
        if ($stack->count() != 1) {
3784
            return $this->raiseFormulaError('internal error');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->raiseFormulaError('internal error') targeting PhpOffice\PhpSpreadsheet...on::raiseFormulaError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3785
        }
3786 118
        $output = $stack->pop();
3787 118
        $output = $output['value'];
3788
3789 118
        return $output;
3790
    }
3791
3792 31
    private function validateBinaryOperand(&$operand, &$stack)
3793
    {
3794 31
        if (is_array($operand)) {
3795 22
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
3796
                do {
3797 22
                    $operand = array_pop($operand);
3798 22
                } while (is_array($operand));
3799
            }
3800
        }
3801
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
3802 31
        if (is_string($operand)) {
3803
            //    We only need special validations for the operand if it is a string
3804
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
3805 2
            if ($operand > '' && $operand[0] == '"') {
3806
                $operand = self::unwrapResult($operand);
3807
            }
3808
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
3809 2
            if (!is_numeric($operand)) {
3810
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
3811 2
                if ($operand > '' && $operand[0] == '#') {
3812
                    $stack->push('Value', $operand);
3813
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand));
3814
3815
                    return false;
3816 2
                } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
3817
                    //    If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
3818 2
                    $stack->push('Value', '#VALUE!');
3819 2
                    $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!'));
3820
3821 2
                    return false;
3822
                }
3823
            }
3824
        }
3825
3826
        //    return a true if the value of the operand is one that we can use in normal binary operations
3827 30
        return true;
3828
    }
3829
3830
    /**
3831
     * @param null|string $cellID
3832
     * @param mixed $operand1
3833
     * @param mixed $operand2
3834
     * @param string $operation
3835
     * @param Stack $stack
3836
     * @param bool $recursingArrays
3837
     *
3838
     * @return bool
3839
     */
3840 74
    private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
3841
    {
3842
        //    If we're dealing with matrix operations, we want a matrix result
3843 74
        if ((is_array($operand1)) || (is_array($operand2))) {
3844 12
            $result = [];
3845 12
            if ((is_array($operand1)) && (!is_array($operand2))) {
3846 12
                foreach ($operand1 as $x => $operandData) {
3847 12
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2));
3848 12
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack);
3849 12
                    $r = $stack->pop();
3850 12
                    $result[$x] = $r['value'];
3851
                }
3852
            } elseif ((!is_array($operand1)) && (is_array($operand2))) {
3853
                foreach ($operand2 as $x => $operandData) {
3854
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData));
3855
                    $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack);
3856
                    $r = $stack->pop();
3857
                    $result[$x] = $r['value'];
3858
                }
3859
            } else {
3860
                if (!$recursingArrays) {
3861
                    self::checkMatrixOperands($operand1, $operand2, 2);
3862
                }
3863
                foreach ($operand1 as $x => $operandData) {
3864
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x]));
3865
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true);
3866
                    $r = $stack->pop();
3867
                    $result[$x] = $r['value'];
3868
                }
3869
            }
3870
            //    Log the result details
3871 12
            $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result));
3872
            //    And push the result onto the stack
3873 12
            $stack->push('Array', $result);
3874
3875 12
            return true;
3876
        }
3877
3878
        //    Simple validate the two operands if they are string values
3879 74
        if (is_string($operand1) && $operand1 > '' && $operand1[0] == '"') {
3880 43
            $operand1 = self::unwrapResult($operand1);
3881
        }
3882 74
        if (is_string($operand2) && $operand2 > '' && $operand2[0] == '"') {
3883 55
            $operand2 = self::unwrapResult($operand2);
3884
        }
3885
3886
        // Use case insensitive comparaison if not OpenOffice mode
3887 74
        if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
3888 74
            if (is_string($operand1)) {
3889 43
                $operand1 = strtoupper($operand1);
3890
            }
3891 74
            if (is_string($operand2)) {
3892 55
                $operand2 = strtoupper($operand2);
3893
            }
3894
        }
3895
3896 74
        $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE;
3897
3898
        //    execute the necessary operation
3899
        switch ($operation) {
3900
            //    Greater than
3901 74
            case '>':
3902 18
                if ($useLowercaseFirstComparison) {
3903 9
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0;
3904
                } else {
3905 18
                    $result = ($operand1 > $operand2);
3906
                }
3907
3908 18
                break;
3909
            //    Less than
3910 60
            case '<':
3911 8
                if ($useLowercaseFirstComparison) {
3912 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0;
3913
                } else {
3914 8
                    $result = ($operand1 < $operand2);
3915
                }
3916
3917 8
                break;
3918
            //    Equality
3919 53
            case '=':
3920 19
                if (is_numeric($operand1) && is_numeric($operand2)) {
3921 3
                    $result = (abs($operand1 - $operand2) < $this->delta);
3922
                } else {
3923 16
                    $result = strcmp($operand1, $operand2) == 0;
3924
                }
3925
3926 19
                break;
3927
            //    Greater than or equal
3928 34
            case '>=':
3929 8
                if (is_numeric($operand1) && is_numeric($operand2)) {
3930 4
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2));
3931 4
                } elseif ($useLowercaseFirstComparison) {
3932 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0;
3933
                } else {
3934 4
                    $result = strcmp($operand1, $operand2) >= 0;
3935
                }
3936
3937 8
                break;
3938
            //    Less than or equal
3939 26
            case '<=':
3940 7
                if (is_numeric($operand1) && is_numeric($operand2)) {
3941 3
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2));
3942 4
                } elseif ($useLowercaseFirstComparison) {
3943 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0;
3944
                } else {
3945 4
                    $result = strcmp($operand1, $operand2) <= 0;
3946
                }
3947
3948 7
                break;
3949
            //    Inequality
3950 19
            case '<>':
3951 19
                if (is_numeric($operand1) && is_numeric($operand2)) {
3952 3
                    $result = (abs($operand1 - $operand2) > 1E-14);
3953
                } else {
3954 16
                    $result = strcmp($operand1, $operand2) != 0;
3955
                }
3956
3957 19
                break;
3958
        }
3959
3960
        //    Log the result details
3961 74
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
3962
        //    And push the result onto the stack
3963 74
        $stack->push('Value', $result);
3964
3965 74
        return true;
3966
    }
3967
3968
    /**
3969
     * Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
3970
     *
3971
     * @param string $str1 First string value for the comparison
3972
     * @param string $str2 Second string value for the comparison
3973
     *
3974
     * @return int
3975
     */
3976 21
    private function strcmpLowercaseFirst($str1, $str2)
3977
    {
3978 21
        $inversedStr1 = Shared\StringHelper::strCaseReverse($str1);
3979 21
        $inversedStr2 = Shared\StringHelper::strCaseReverse($str2);
3980
3981 21
        return strcmp($inversedStr1, $inversedStr2);
3982
    }
3983
3984
    /**
3985
     * @param mixed $operand1
3986
     * @param mixed $operand2
3987
     * @param mixed $operation
3988
     * @param string $matrixFunction
3989
     * @param mixed $stack
3990
     *
3991
     * @return bool
3992
     */
3993 31
    private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack)
3994
    {
3995
        //    Validate the two operands
3996 31
        if (!$this->validateBinaryOperand($operand1, $stack)) {
3997 2
            return false;
3998
        }
3999 30
        if (!$this->validateBinaryOperand($operand2, $stack)) {
4000 1
            return false;
4001
        }
4002
4003
        //    If either of the operands is a matrix, we need to treat them both as matrices
4004
        //        (converting the other operand to a matrix if need be); then perform the required
4005
        //        matrix operation
4006 30
        if ((is_array($operand1)) || (is_array($operand2))) {
4007
            //    Ensure that both operands are arrays/matrices of the same size
4008
            self::checkMatrixOperands($operand1, $operand2, 2);
4009
4010
            try {
4011
                //    Convert operand 1 from a PHP array to a matrix
4012
                $matrix = new Shared\JAMA\Matrix($operand1);
4013
                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4014
                $matrixResult = $matrix->$matrixFunction($operand2);
4015
                $result = $matrixResult->getArray();
4016
            } catch (\Exception $ex) {
4017
                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4018
                $result = '#VALUE!';
4019
            }
4020
        } else {
4021 30
            if ((Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) &&
4022 30
                ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) ||
4023 30
                 (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0))) {
4024
                $result = Functions::VALUE();
4025
            } else {
4026
                //    If we're dealing with non-matrix operations, execute the necessary operation
4027
                switch ($operation) {
4028
                    //    Addition
4029 30
                    case '+':
4030 22
                        $result = $operand1 + $operand2;
4031
4032 22
                        break;
4033
                    //    Subtraction
4034 26
                    case '-':
4035 7
                        $result = $operand1 - $operand2;
4036
4037 7
                        break;
4038
                    //    Multiplication
4039 24
                    case '*':
4040 19
                        $result = $operand1 * $operand2;
4041
4042 19
                        break;
4043
                    //    Division
4044 8
                    case '/':
4045 8
                        if ($operand2 == 0) {
4046
                            //    Trap for Divide by Zero error
4047 8
                            $stack->push('Value', '#DIV/0!');
4048 8
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails('#DIV/0!'));
4049
4050 8
                            return false;
4051
                        }
4052
                            $result = $operand1 / $operand2;
4053
4054
                        break;
4055
                    //    Power
4056
                    case '^':
4057
                        $result = pow($operand1, $operand2);
4058
4059
                        break;
4060
                }
4061
            }
4062
        }
4063
4064
        //    Log the result details
4065 25
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
4066
        //    And push the result onto the stack
4067 25
        $stack->push('Value', $result);
4068
4069 25
        return true;
4070
    }
4071
4072
    // trigger an error, but nicely, if need be
4073
    protected function raiseFormulaError($errorMessage)
4074
    {
4075
        $this->formulaError = $errorMessage;
4076
        $this->cyclicReferenceStack->clear();
4077
        if (!$this->suppressFormulaErrors) {
4078
            throw new Exception($errorMessage);
4079
        }
4080
        trigger_error($errorMessage, E_USER_ERROR);
4081
    }
4082
4083
    /**
4084
     * Extract range values.
4085
     *
4086
     * @param string &$pRange String based range representation
4087
     * @param Worksheet $pSheet Worksheet
4088
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4089
     *
4090
     * @throws Exception
4091
     *
4092
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4093
     */
4094 60
    public function extractCellRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4095
    {
4096
        // Return value
4097 60
        $returnValue = [];
4098
4099 60
        if ($pSheet !== null) {
4100 60
            $pSheetName = $pSheet->getTitle();
4101 60
            if (strpos($pRange, '!') !== false) {
4102
                list($pSheetName, $pRange) = Worksheet::extractSheetTitle($pRange, true);
4103
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4104
            }
4105
4106
            // Extract range
4107 60
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4108 60
            $pRange = $pSheetName . '!' . $pRange;
4109 60
            if (!isset($aReferences[1])) {
4110
                //    Single cell in range
4111 60
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $currentRow seems to be never defined.
Loading history...
4112 60
                if ($pSheet->cellExists($aReferences[0])) {
4113 60
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4114
                } else {
4115 60
                    $returnValue[$currentRow][$currentCol] = null;
4116
                }
4117
            } else {
4118
                // Extract cell data for all cells in the range
4119 52
                foreach ($aReferences as $reference) {
4120
                    // Extract range
4121 52
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
4122 52
                    if ($pSheet->cellExists($reference)) {
4123 52
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4124
                    } else {
4125 52
                        $returnValue[$currentRow][$currentCol] = null;
4126
                    }
4127
                }
4128
            }
4129
        }
4130
4131 60
        return $returnValue;
4132
    }
4133
4134
    /**
4135
     * Extract range values.
4136
     *
4137
     * @param string &$pRange String based range representation
4138
     * @param Worksheet $pSheet Worksheet
4139
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4140
     *
4141
     * @throws Exception
4142
     *
4143
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4144
     */
4145 5
    public function extractNamedRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4146
    {
4147
        // Return value
4148 5
        $returnValue = [];
4149
4150 5
        if ($pSheet !== null) {
4151 5
            $pSheetName = $pSheet->getTitle();
0 ignored issues
show
Unused Code introduced by
The assignment to $pSheetName is dead and can be removed.
Loading history...
4152 5
            if (strpos($pRange, '!') !== false) {
4153
                list($pSheetName, $pRange) = Worksheet::extractSheetTitle($pRange, true);
4154
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4155
            }
4156
4157
            // Named range?
4158 5
            $namedRange = NamedRange::resolveRange($pRange, $pSheet);
4159 5
            if ($namedRange !== null) {
4160 2
                $pSheet = $namedRange->getWorksheet();
4161 2
                $pRange = $namedRange->getRange();
4162 2
                $splitRange = Coordinate::splitRange($pRange);
4163
                //    Convert row and column references
4164 2
                if (ctype_alpha($splitRange[0][0])) {
4165
                    $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
4166 2
                } elseif (ctype_digit($splitRange[0][0])) {
4167 2
                    $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
4168
                }
4169
            } else {
4170 3
                return Functions::REF();
4171
            }
4172
4173
            // Extract range
4174 2
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4175 2
            if (!isset($aReferences[1])) {
4176
                //    Single cell (or single column or row) in range
4177 1
                list($currentCol, $currentRow) = Coordinate::coordinateFromString($aReferences[0]);
4178 1
                if ($pSheet->cellExists($aReferences[0])) {
4179 1
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4180
                } else {
4181 1
                    $returnValue[$currentRow][$currentCol] = null;
4182
                }
4183
            } else {
4184
                // Extract cell data for all cells in the range
4185 1
                foreach ($aReferences as $reference) {
4186
                    // Extract range
4187 1
                    list($currentCol, $currentRow) = Coordinate::coordinateFromString($reference);
4188 1
                    if ($pSheet->cellExists($reference)) {
4189 1
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4190
                    } else {
4191 1
                        $returnValue[$currentRow][$currentCol] = null;
4192
                    }
4193
                }
4194
            }
4195
        }
4196
4197 2
        return $returnValue;
4198
    }
4199
4200
    /**
4201
     * Is a specific function implemented?
4202
     *
4203
     * @param string $pFunction Function Name
4204
     *
4205
     * @return bool
4206
     */
4207 3
    public function isImplemented($pFunction)
4208
    {
4209 3
        $pFunction = strtoupper($pFunction);
4210 3
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
4211
4212 3
        return !$notImplemented;
4213
    }
4214
4215
    /**
4216
     * Get a list of all implemented functions as an array of function objects.
4217
     *
4218
     * @return array of Category
4219
     */
4220
    public function getFunctions()
4221
    {
4222
        return self::$phpSpreadsheetFunctions;
4223
    }
4224
4225
    /**
4226
     * Get a list of implemented Excel function names.
4227
     *
4228
     * @return array
4229
     */
4230 2
    public function getImplementedFunctionNames()
4231
    {
4232 2
        $returnValue = [];
4233 2
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
4234 2
            if ($this->isImplemented($functionName)) {
4235 2
                $returnValue[] = $functionName;
4236
            }
4237
        }
4238
4239 2
        return $returnValue;
4240
    }
4241
}
4242