Completed
Push — develop ( 13472e...0d1ff5 )
by Adrien
18:47
created

Calculation::mkMatrix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2036 71
        $this->cyclicReferenceStack = new CalcEngine\CyclicReferenceStack();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \PhpOffice\PhpSpread...\CyclicReferenceStack() of type object<PhpOffice\PhpSpre...e\CyclicReferenceStack> is incompatible with the declared type array of property $cyclicReferenceStack.

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

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

Loading history...
2037 71
        $this->debugLog = new CalcEngine\Logger($this->cyclicReferenceStack);
2038 71
    }
2039
2040 1
    private static function loadLocales()
2041
    {
2042 1
        $localeFileDirectory = __DIR__ . '/locale/';
2043 1
        foreach (glob($localeFileDirectory . '/*', GLOB_ONLYDIR) as $filename) {
2044 1
            $filename = substr($filename, strlen($localeFileDirectory) + 1);
2045 1
            if ($filename != 'en') {
2046 1
                self::$validLocaleLanguages[] = $filename;
2047
            }
2048
        }
2049 1
    }
2050
2051
    /**
2052
     * Get an instance of this class.
2053
     *
2054
     * @param Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object,
2055
     *                                    or NULL to create a standalone claculation engine
2056
     *
2057
     * @return Calculation
2058
     */
2059 139
    public static function getInstance(Spreadsheet $spreadsheet = null)
2060
    {
2061 139
        if ($spreadsheet !== null) {
2062 63
            $instance = $spreadsheet->getCalculationEngine();
2063 63
            if (isset($instance)) {
2064 63
                return $instance;
2065
            }
2066
        }
2067
2068 79
        if (!isset(self::$instance) || (self::$instance === null)) {
2069 3
            self::$instance = new \PhpOffice\PhpSpreadsheet\Calculation();
2070
        }
2071
2072 79
        return self::$instance;
2073
    }
2074
2075
    /**
2076
     * Unset an instance of this class.
2077
     *
2078
     * @param Spreadsheet $spreadsheet Injected spreadsheet identifying the instance to unset
0 ignored issues
show
Bug introduced by
There is no parameter named $spreadsheet. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
2079
     */
2080 1
    public function __destruct()
2081
    {
2082 1
        $this->workbook = null;
0 ignored issues
show
Bug introduced by
The property workbook does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2083 1
    }
2084
2085
    /**
2086
     * Flush the calculation cache for any existing instance of this class
2087
     *        but only if a \PhpOffice\PhpSpreadsheet\Calculation instance exists.
2088
     */
2089
    public function flushInstance()
2090
    {
2091
        $this->clearCalculationCache();
2092
    }
2093
2094
    /**
2095
     * Get the Logger for this calculation engine instance.
2096
     *
2097
     * @return CalcEngine\Logger
2098
     */
2099 59
    public function getDebugLog()
2100
    {
2101 59
        return $this->debugLog;
2102
    }
2103
2104
    /**
2105
     * __clone implementation. Cloning should not be allowed in a Singleton!
2106
     *
2107
     * @throws Calculation\Exception
2108
     */
2109
    final public function __clone()
2110
    {
2111
        throw new Calculation\Exception('Cloning the calculation engine is not allowed!');
2112
    }
2113
2114
    /**
2115
     * Return the locale-specific translation of TRUE.
2116
     *
2117
     * @return string locale-specific translation of TRUE
2118
     */
2119 36
    public static function getTRUE()
2120
    {
2121 36
        return self::$localeBoolean['TRUE'];
2122
    }
2123
2124
    /**
2125
     * Return the locale-specific translation of FALSE.
2126
     *
2127
     * @return string locale-specific translation of FALSE
2128
     */
2129 29
    public static function getFALSE()
2130
    {
2131 29
        return self::$localeBoolean['FALSE'];
2132
    }
2133
2134
    /**
2135
     * Set the Array Return Type (Array or Value of first element in the array).
2136
     *
2137
     * @param string $returnType Array return type
2138
     *
2139
     * @return bool Success or failure
2140
     */
2141 18
    public static function setArrayReturnType($returnType)
2142
    {
2143 18
        if (($returnType == self::RETURN_ARRAY_AS_VALUE) ||
2144 11
            ($returnType == self::RETURN_ARRAY_AS_ERROR) ||
2145 18
            ($returnType == self::RETURN_ARRAY_AS_ARRAY)) {
2146 18
            self::$returnArrayAsType = $returnType;
2147
2148 18
            return true;
2149
        }
2150
2151
        return false;
2152
    }
2153
2154
    /**
2155
     * Return the Array Return Type (Array or Value of first element in the array).
2156
     *
2157
     * @return string $returnType Array return type
2158
     */
2159 7
    public static function getArrayReturnType()
2160
    {
2161 7
        return self::$returnArrayAsType;
2162
    }
2163
2164
    /**
2165
     * Is calculation caching enabled?
2166
     *
2167
     * @return bool
2168
     */
2169
    public function getCalculationCacheEnabled()
2170
    {
2171
        return $this->calculationCacheEnabled;
2172
    }
2173
2174
    /**
2175
     * Enable/disable calculation cache.
2176
     *
2177
     * @param bool $pValue
2178
     */
2179
    public function setCalculationCacheEnabled($pValue = true)
2180
    {
2181
        $this->calculationCacheEnabled = $pValue;
2182
        $this->clearCalculationCache();
2183
    }
2184
2185
    /**
2186
     * Enable calculation cache.
2187
     */
2188
    public function enableCalculationCache()
2189
    {
2190
        $this->setCalculationCacheEnabled(true);
2191
    }
2192
2193
    /**
2194
     * Disable calculation cache.
2195
     */
2196
    public function disableCalculationCache()
2197
    {
2198
        $this->setCalculationCacheEnabled(false);
2199
    }
2200
2201
    /**
2202
     * Clear calculation cache.
2203
     */
2204
    public function clearCalculationCache()
2205
    {
2206
        $this->calculationCache = [];
2207
    }
2208
2209
    /**
2210
     * Clear calculation cache for a specified worksheet.
2211
     *
2212
     * @param string $worksheetName
2213
     */
2214 1
    public function clearCalculationCacheForWorksheet($worksheetName)
2215
    {
2216 1
        if (isset($this->calculationCache[$worksheetName])) {
2217
            unset($this->calculationCache[$worksheetName]);
2218
        }
2219 1
    }
2220
2221
    /**
2222
     * Rename calculation cache for a specified worksheet.
2223
     *
2224
     * @param string $fromWorksheetName
2225
     * @param string $toWorksheetName
2226
     */
2227 71
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName)
2228
    {
2229 71
        if (isset($this->calculationCache[$fromWorksheetName])) {
2230
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2231
            unset($this->calculationCache[$fromWorksheetName]);
2232
        }
2233 71
    }
2234
2235
    /**
2236
     * Get the currently defined locale code.
2237
     *
2238
     * @return string
2239
     */
2240
    public function getLocale()
2241
    {
2242
        return self::$localeLanguage;
2243
    }
2244
2245
    /**
2246
     * Set the locale code.
2247
     *
2248
     * @param string $locale The locale to use for formula translation
2249
     *
2250
     * @return bool
2251
     */
2252 18
    public function setLocale($locale = 'en_us')
2253
    {
2254
        //    Identify our locale and language
2255 18
        $language = $locale = strtolower($locale);
2256 18
        if (strpos($locale, '_') !== false) {
2257 2
            list($language) = explode('_', $locale);
2258
        }
2259
2260 18
        if (count(self::$validLocaleLanguages) == 1) {
2261 1
            self::loadLocales();
2262
        }
2263
        //    Test whether we have any language data for this language (any locale)
2264 18
        if (in_array($language, self::$validLocaleLanguages)) {
2265
            //    initialise language/locale settings
2266 18
            self::$localeFunctions = [];
2267 18
            self::$localeArgumentSeparator = ',';
2268 18
            self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL'];
0 ignored issues
show
Documentation Bug introduced by
It seems like array('TRUE' => 'TRUE', ...LSE', 'NULL' => 'NULL') of type array<string,string,{"TR...ring","NULL":"string"}> is incompatible with the declared type array<integer,string> of property $localeBoolean.

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

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

Loading history...
2269
            //    Default is English, if user isn't requesting english, then read the necessary data from the locale files
2270 18
            if ($locale != 'en_us') {
2271
                //    Search for a file with a list of function names for locale
2272 17
                $functionNamesFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'functions';
2273 17
                if (!file_exists($functionNamesFile)) {
2274
                    //    If there isn't a locale specific function file, look for a language specific function file
2275
                    $functionNamesFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'functions';
2276
                    if (!file_exists($functionNamesFile)) {
2277
                        return false;
2278
                    }
2279
                }
2280
                //    Retrieve the list of locale or language specific function names
2281 17
                $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2282 17
                foreach ($localeFunctions as $localeFunction) {
2283 17
                    list($localeFunction) = explode('##', $localeFunction); //    Strip out comments
2284 17
                    if (strpos($localeFunction, '=') !== false) {
2285 17
                        list($fName, $lfName) = explode('=', $localeFunction);
2286 17
                        $fName = trim($fName);
2287 17
                        $lfName = trim($lfName);
2288 17
                        if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) {
2289 17
                            self::$localeFunctions[$fName] = $lfName;
2290
                        }
2291
                    }
2292
                }
2293
                //    Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions
2294 17
                if (isset(self::$localeFunctions['TRUE'])) {
2295 17
                    self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE'];
2296
                }
2297 17
                if (isset(self::$localeFunctions['FALSE'])) {
2298 17
                    self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE'];
2299
                }
2300
2301 17
                $configFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'config';
2302 17
                if (!file_exists($configFile)) {
2303
                    $configFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'config';
2304
                }
2305 17
                if (file_exists($configFile)) {
2306 17
                    $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2307 17
                    foreach ($localeSettings as $localeSetting) {
2308 17
                        list($localeSetting) = explode('##', $localeSetting); //    Strip out comments
2309 17
                        if (strpos($localeSetting, '=') !== false) {
2310 17
                            list($settingName, $settingValue) = explode('=', $localeSetting);
2311 17
                            $settingName = strtoupper(trim($settingName));
2312
                            switch ($settingName) {
2313 17
                                case 'ARGUMENTSEPARATOR':
2314 17
                                    self::$localeArgumentSeparator = trim($settingValue);
2315 17
                                    break;
2316
                            }
2317
                        }
2318
                    }
2319
                }
2320
            }
2321
2322
            self::$functionReplaceFromExcel = self::$functionReplaceToExcel =
2323 18
            self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
2324 18
            self::$localeLanguage = $locale;
2325
2326 18
            return true;
2327
        }
2328
2329
        return false;
2330
    }
2331
2332 5
    public static function translateSeparator($fromSeparator, $toSeparator, $formula, &$inBraces)
2333
    {
2334 5
        $strlen = mb_strlen($formula);
2335 5
        for ($i = 0; $i < $strlen; ++$i) {
2336 5
            $chr = mb_substr($formula, $i, 1);
2337
            switch ($chr) {
2338 5
                case '{':
2339
                    $inBraces = true;
2340
                    break;
2341 5
                case '}':
2342
                    $inBraces = false;
2343
                    break;
2344 5
                case $fromSeparator:
2345
                    if (!$inBraces) {
2346
                        $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
2347
                    }
2348
            }
2349
        }
2350
2351 5
        return $formula;
2352
    }
2353
2354
    /**
2355
     * @param string $fromSeparator
2356
     * @param string $toSeparator
2357
     * @param mixed $from
2358
     * @param mixed $to
2359
     * @param mixed $formula
2360
     */
2361
    private static function translateFormula($from, $to, $formula, $fromSeparator, $toSeparator)
2362
    {
2363
        //    Convert any Excel function names to the required language
2364
        if (self::$localeLanguage !== 'en_us') {
2365
            $inBraces = false;
2366
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
2367
            if (strpos($formula, '"') !== false) {
2368
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
2369
                //        the formula
2370
                $temp = explode('"', $formula);
2371
                $i = false;
2372
                foreach ($temp as &$value) {
2373
                    //    Only count/replace in alternating array entries
2374 View Code Duplication
                    if ($i = !$i) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2375
                        $value = preg_replace($from, $to, $value);
2376
                        $value = self::translateSeparator($fromSeparator, $toSeparator, $value, $inBraces);
2377
                    }
2378
                }
2379
                unset($value);
2380
                //    Then rebuild the formula string
2381
                $formula = implode('"', $temp);
2382 View Code Duplication
            } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2383
                //    If there's no quoted strings, then we do a simple count/replace
2384
                $formula = preg_replace($from, $to, $formula);
2385
                $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inBraces);
2386
            }
2387
        }
2388
2389
        return $formula;
2390
    }
2391
2392
    private static $functionReplaceFromExcel = null;
2393
    private static $functionReplaceToLocale = null;
2394
2395 View Code Duplication
    public function _translateFormulaToLocale($formula)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2396
    {
2397
        if (self::$functionReplaceFromExcel === null) {
2398
            self::$functionReplaceFromExcel = [];
2399
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2400
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName) . '([\s]*\()/Ui';
2401
            }
2402
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2403
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean) . '([^\w\.])/Ui';
2404
            }
2405
        }
2406
2407
        if (self::$functionReplaceToLocale === null) {
2408
            self::$functionReplaceToLocale = [];
2409
            foreach (array_values(self::$localeFunctions) as $localeFunctionName) {
2410
                self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2';
2411
            }
2412
            foreach (array_values(self::$localeBoolean) as $localeBoolean) {
2413
                self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2';
2414
            }
2415
        }
2416
2417
        return self::translateFormula(self::$functionReplaceFromExcel, self::$functionReplaceToLocale, $formula, ',', self::$localeArgumentSeparator);
2418
    }
2419
2420
    private static $functionReplaceFromLocale = null;
2421
    private static $functionReplaceToExcel = null;
2422
2423 View Code Duplication
    public function _translateFormulaToEnglish($formula)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2424
    {
2425
        if (self::$functionReplaceFromLocale === null) {
2426
            self::$functionReplaceFromLocale = [];
2427
            foreach (array_values(self::$localeFunctions) as $localeFunctionName) {
2428
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName) . '([\s]*\()/Ui';
2429
            }
2430
            foreach (array_values(self::$localeBoolean) as $excelBoolean) {
2431
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean) . '([^\w\.])/Ui';
2432
            }
2433
        }
2434
2435
        if (self::$functionReplaceToExcel === null) {
2436
            self::$functionReplaceToExcel = [];
2437
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2438
                self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2';
2439
            }
2440
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2441
                self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2';
2442
            }
2443
        }
2444
2445
        return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ',');
2446
    }
2447
2448 24
    public static function localeFunc($function)
2449
    {
2450 24
        if (self::$localeLanguage !== 'en_us') {
2451
            $functionName = trim($function, '(');
2452
            if (isset(self::$localeFunctions[$functionName])) {
2453
                $brace = ($functionName != $function);
2454
                $function = self::$localeFunctions[$functionName];
2455
                if ($brace) {
2456
                    $function .= '(';
2457
                }
2458
            }
2459
        }
2460
2461 24
        return $function;
2462
    }
2463
2464
    /**
2465
     * Wrap string values in quotes.
2466
     *
2467
     * @param mixed $value
2468
     *
2469
     * @return mixed
2470
     */
2471 58
    public static function wrapResult($value)
2472
    {
2473 58
        if (is_string($value)) {
2474
            //    Error values cannot be "wrapped"
2475 48
            if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
2476
                //    Return Excel errors "as is"
2477 1
                return $value;
2478
            }
2479
            //    Return strings wrapped in quotes
2480 48
            return '"' . $value . '"';
2481
        //    Convert numeric errors to NaN error
2482 23
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2483
            return Calculation\Functions::NAN();
2484
        }
2485
2486 23
        return $value;
2487
    }
2488
2489
    /**
2490
     * Remove quotes used as a wrapper to identify string values.
2491
     *
2492
     * @param mixed $value
2493
     *
2494
     * @return mixed
2495
     */
2496 70
    public static function unwrapResult($value)
2497
    {
2498 70
        if (is_string($value)) {
2499 52
            if ((isset($value[0])) && ($value[0] == '"') && (substr($value, -1) == '"')) {
2500 52
                return substr($value, 1, -1);
2501
            }
2502
        //    Convert numeric errors to NAN error
2503 36
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2504
            return Calculation\Functions::NAN();
2505
        }
2506
2507 36
        return $value;
2508
    }
2509
2510
    /**
2511
     * Calculate cell value (using formula from a cell ID)
2512
     * Retained for backward compatibility.
2513
     *
2514
     * @param Cell $pCell Cell to calculate
2515
     *
2516
     * @throws Calculation\Exception
2517
     *
2518
     * @return mixed
2519
     */
2520
    public function calculate(Cell $pCell = null)
2521
    {
2522
        try {
2523
            return $this->calculateCellValue($pCell);
2524
        } catch (Exception $e) {
2525
            throw new Calculation\Exception($e->getMessage());
2526
        }
2527
    }
2528
2529
    /**
2530
     * Calculate the value of a cell formula.
2531
     *
2532
     * @param Cell $pCell Cell to calculate
2533
     * @param bool $resetLog Flag indicating whether the debug log should be reset or not
2534
     *
2535
     * @throws Calculation\Exception
2536
     *
2537
     * @return mixed
2538
     */
2539 25
    public function calculateCellValue(Cell $pCell = null, $resetLog = true)
2540
    {
2541 25
        if ($pCell === null) {
2542
            return null;
2543
        }
2544
2545 25
        $returnArrayAsType = self::$returnArrayAsType;
2546 25
        if ($resetLog) {
2547
            //    Initialise the logging settings if requested
2548 25
            $this->formulaError = null;
2549 25
            $this->debugLog->clearLog();
2550 25
            $this->cyclicReferenceStack->clear();
0 ignored issues
show
Bug introduced by
The method clear cannot be called on $this->cyclicReferenceStack (of type array).

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

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

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

Loading history...
2649
2650
        if ($this->spreadsheet !== null && $cellID === null && $pCell === null) {
2651
            $cellID = 'A1';
2652
            $pCell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
2653
        } else {
2654
            //    Disable calculation cacheing because it only applies to cell calculations, not straight formulae
2655
            //    But don't actually flush any cache
2656
            $resetCache = $this->getCalculationCacheEnabled();
2657
            $this->calculationCacheEnabled = false;
2658
        }
2659
2660
        //    Execute the calculation
2661
        try {
2662
            $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $pCell));
2663
        } catch (Exception $e) {
2664
            throw new Calculation\Exception($e->getMessage());
2665
        }
2666
2667
        if ($this->spreadsheet === null) {
2668
            //    Reset calculation cacheing to its previous state
2669
            $this->calculationCacheEnabled = $resetCache;
0 ignored issues
show
Bug introduced by
The variable $resetCache does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Loading history...
2737
            if ($this->cyclicFormulaCount <= 0) {
2738
                $this->cyclicFormulaCell = '';
2739
2740
                return $this->raiseFormulaError('Cyclic Reference in Formula');
2741
            } elseif ($this->cyclicFormulaCell === $wsCellReference) {
2742
                ++$this->cyclicFormulaCounter;
2743
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2744
                    $this->cyclicFormulaCell = '';
2745
2746
                    return $cellValue;
2747
                }
2748
            } elseif ($this->cyclicFormulaCell == '') {
2749
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2750
                    return $cellValue;
2751
                }
2752
                $this->cyclicFormulaCell = $wsCellReference;
2753
            }
2754
        }
2755
2756
        //    Parse the formula onto the token stack and calculate the value
2757 94
        $this->cyclicReferenceStack->push($wsCellReference);
0 ignored issues
show
Bug introduced by
The method push cannot be called on $this->cyclicReferenceStack (of type array).

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

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

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

Loading history...
2760
2761
        // Save to calculation cache
2762 94
        if ($cellID !== null) {
2763 25
            $this->saveValueToCache($wsCellReference, $cellValue);
2764
        }
2765
2766
        //    Return the calculated value
2767 94
        return $cellValue;
2768
    }
2769
2770
    /**
2771
     * Ensure that paired matrix operands are both matrices and of the same size.
2772
     *
2773
     * @param mixed &$operand1 First matrix operand
2774
     * @param mixed &$operand2 Second matrix operand
2775
     * @param int $resize Flag indicating whether the matrices should be resized to match
2776
     *                                        and (if so), whether the smaller dimension should grow or the
2777
     *                                        larger should shrink.
2778
     *                                            0 = no resize
2779
     *                                            1 = shrink to fit
2780
     *                                            2 = extend to fit
2781
     */
2782 5
    private static function checkMatrixOperands(&$operand1, &$operand2, $resize = 1)
2783
    {
2784
        //    Examine each of the two operands, and turn them into an array if they aren't one already
2785
        //    Note that this function should only be called if one or both of the operand is already an array
2786 5
        if (!is_array($operand1)) {
2787
            list($matrixRows, $matrixColumns) = self::getMatrixDimensions($operand2);
2788
            $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1));
2789
            $resize = 0;
2790 5
        } elseif (!is_array($operand2)) {
2791 2
            list($matrixRows, $matrixColumns) = self::getMatrixDimensions($operand1);
2792 2
            $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2));
2793 2
            $resize = 0;
2794
        }
2795
2796 5
        list($matrix1Rows, $matrix1Columns) = self::getMatrixDimensions($operand1);
2797 5
        list($matrix2Rows, $matrix2Columns) = self::getMatrixDimensions($operand2);
2798 5
        if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
2799 5
            $resize = 1;
2800
        }
2801
2802 5
        if ($resize == 2) {
2803
            //    Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger
2804
            self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2805 5
        } elseif ($resize == 1) {
2806
            //    Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller
2807 5
            self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2808
        }
2809
2810 5
        return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns];
2811
    }
2812
2813
    /**
2814
     * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0.
2815
     *
2816
     * @param mixed &$matrix matrix operand
2817
     *
2818
     * @return int[] An array comprising the number of rows, and number of columns
2819
     */
2820 5
    private static function getMatrixDimensions(&$matrix)
2821
    {
2822 5
        $matrixRows = count($matrix);
2823 5
        $matrixColumns = 0;
2824 5
        foreach ($matrix as $rowKey => $rowValue) {
2825 5
            $matrixColumns = max(count($rowValue), $matrixColumns);
2826 5
            if (!is_array($rowValue)) {
2827
                $matrix[$rowKey] = [$rowValue];
2828
            } else {
2829 5
                $matrix[$rowKey] = array_values($rowValue);
2830
            }
2831
        }
2832 5
        $matrix = array_values($matrix);
2833
2834 5
        return [$matrixRows, $matrixColumns];
2835
    }
2836
2837
    /**
2838
     * Ensure that paired matrix operands are both matrices of the same size.
2839
     *
2840
     * @param mixed &$matrix1 First matrix operand
2841
     * @param mixed &$matrix2 Second matrix operand
2842
     * @param int $matrix1Rows Row size of first matrix operand
2843
     * @param int $matrix1Columns Column size of first matrix operand
2844
     * @param int $matrix2Rows Row size of second matrix operand
2845
     * @param int $matrix2Columns Column size of second matrix operand
2846
     */
2847 5
    private static function resizeMatricesShrink(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
2848
    {
2849 5 View Code Duplication
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2850
            if ($matrix2Rows < $matrix1Rows) {
2851
                for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) {
2852
                    unset($matrix1[$i]);
2853
                }
2854
            }
2855
            if ($matrix2Columns < $matrix1Columns) {
2856
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2857
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
2858
                        unset($matrix1[$i][$j]);
2859
                    }
2860
                }
2861
            }
2862
        }
2863
2864 5 View Code Duplication
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2865
            if ($matrix1Rows < $matrix2Rows) {
2866
                for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) {
2867
                    unset($matrix2[$i]);
2868
                }
2869
            }
2870
            if ($matrix1Columns < $matrix2Columns) {
2871
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2872
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
2873
                        unset($matrix2[$i][$j]);
2874
                    }
2875
                }
2876
            }
2877
        }
2878 5
    }
2879
2880
    /**
2881
     * Ensure that paired matrix operands are both matrices of the same size.
2882
     *
2883
     * @param mixed &$matrix1 First matrix operand
2884
     * @param mixed &$matrix2 Second matrix operand
2885
     * @param int $matrix1Rows Row size of first matrix operand
2886
     * @param int $matrix1Columns Column size of first matrix operand
2887
     * @param int $matrix2Rows Row size of second matrix operand
2888
     * @param int $matrix2Columns Column size of second matrix operand
2889
     */
2890
    private static function resizeMatricesExtend(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
2891
    {
2892 View Code Duplication
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2893
            if ($matrix2Columns < $matrix1Columns) {
2894
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2895
                    $x = $matrix2[$i][$matrix2Columns - 1];
2896
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
2897
                        $matrix2[$i][$j] = $x;
2898
                    }
2899
                }
2900
            }
2901
            if ($matrix2Rows < $matrix1Rows) {
2902
                $x = $matrix2[$matrix2Rows - 1];
2903
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2904
                    $matrix2[$i] = $x;
2905
                }
2906
            }
2907
        }
2908
2909 View Code Duplication
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2910
            if ($matrix1Columns < $matrix2Columns) {
2911
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2912
                    $x = $matrix1[$i][$matrix1Columns - 1];
2913
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
2914
                        $matrix1[$i][$j] = $x;
2915
                    }
2916
                }
2917
            }
2918
            if ($matrix1Rows < $matrix2Rows) {
2919
                $x = $matrix1[$matrix1Rows - 1];
2920
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2921
                    $matrix1[$i] = $x;
2922
                }
2923
            }
2924
        }
2925
    }
2926
2927
    /**
2928
     * Format details of an operand for display in the log (based on operand type).
2929
     *
2930
     * @param mixed $value First matrix operand
2931
     *
2932
     * @return mixed
2933
     */
2934 93
    private function showValue($value)
2935
    {
2936 93
        if ($this->debugLog->getWriteDebugLog()) {
2937
            $testArray = Calculation\Functions::flattenArray($value);
2938
            if (count($testArray) == 1) {
2939
                $value = array_pop($testArray);
2940
            }
2941
2942
            if (is_array($value)) {
2943
                $returnMatrix = [];
2944
                $pad = $rpad = ', ';
2945
                foreach ($value as $row) {
2946
                    if (is_array($row)) {
2947
                        $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
2948
                        $rpad = '; ';
2949
                    } else {
2950
                        $returnMatrix[] = $this->showValue($row);
2951
                    }
2952
                }
2953
2954
                return '{ ' . implode($rpad, $returnMatrix) . ' }';
2955
            } elseif (is_string($value) && (trim($value, '"') == $value)) {
2956
                return '"' . $value . '"';
2957
            } elseif (is_bool($value)) {
2958
                return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
2959
            }
2960
        }
2961
2962 93
        return Calculation\Functions::flattenSingleValue($value);
2963
    }
2964
2965
    /**
2966
     * Format type and details of an operand for display in the log (based on operand type).
2967
     *
2968
     * @param mixed $value First matrix operand
2969
     *
2970
     * @return string|null
2971
     */
2972 93
    private function showTypeDetails($value)
2973
    {
2974 93
        if ($this->debugLog->getWriteDebugLog()) {
2975
            $testArray = Calculation\Functions::flattenArray($value);
2976
            if (count($testArray) == 1) {
2977
                $value = array_pop($testArray);
2978
            }
2979
2980
            if ($value === null) {
2981
                return 'a NULL value';
2982
            } elseif (is_float($value)) {
2983
                $typeString = 'a floating point number';
2984
            } elseif (is_int($value)) {
2985
                $typeString = 'an integer number';
2986
            } elseif (is_bool($value)) {
2987
                $typeString = 'a boolean';
2988
            } elseif (is_array($value)) {
2989
                $typeString = 'a matrix';
2990
            } else {
2991
                if ($value == '') {
2992
                    return 'an empty string';
2993
                } elseif ($value[0] == '#') {
2994
                    return 'a ' . $value . ' error';
2995
                }
2996
                $typeString = 'a string';
2997
            }
2998
2999
            return $typeString . ' with a value of ' . $this->showValue($value);
3000
        }
3001 93
    }
3002
3003 94
    private function convertMatrixReferences($formula)
3004
    {
3005 94
        static $matrixReplaceFrom = ['{', ';', '}'];
3006 94
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
3007
3008
        //    Convert any Excel matrix references to the MKMATRIX() function
3009 94
        if (strpos($formula, '{') !== false) {
3010
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
3011
            if (strpos($formula, '"') !== false) {
3012
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
3013
                //        the formula
3014
                $temp = explode('"', $formula);
3015
                //    Open and Closed counts used for trapping mismatched braces in the formula
3016
                $openCount = $closeCount = 0;
3017
                $i = false;
3018
                foreach ($temp as &$value) {
3019
                    //    Only count/replace in alternating array entries
3020
                    if ($i = !$i) {
3021
                        $openCount += substr_count($value, '{');
3022
                        $closeCount += substr_count($value, '}');
3023
                        $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
3024
                    }
3025
                }
3026
                unset($value);
3027
                //    Then rebuild the formula string
3028
                $formula = implode('"', $temp);
3029
            } else {
3030
                //    If there's no quoted strings, then we do a simple count/replace
3031
                $openCount = substr_count($formula, '{');
3032
                $closeCount = substr_count($formula, '}');
3033
                $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
3034
            }
3035
            //    Trap for mismatched braces and trigger an appropriate error
3036
            if ($openCount < $closeCount) {
3037
                if ($openCount > 0) {
3038
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
3039
                }
3040
3041
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
3042
            } elseif ($openCount > $closeCount) {
3043
                if ($closeCount > 0) {
3044
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
3045
                }
3046
3047
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
3048
            }
3049
        }
3050
3051 94
        return $formula;
3052
    }
3053
3054
    private static function mkMatrix(...$args)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
3055
    {
3056
        return $args;
3057
    }
3058
3059
    //    Binary Operators
3060
    //    These operators always work on two values
3061
    //    Array key is the operator, the value indicates whether this is a left or right associative operator
3062
    private static $operatorAssociativity = [
3063
        '^' => 0, //    Exponentiation
3064
        '*' => 0, '/' => 0, //    Multiplication and Division
3065
        '+' => 0, '-' => 0, //    Addition and Subtraction
3066
        '&' => 0, //    Concatenation
3067
        '|' => 0, ':' => 0, //    Intersect and Range
3068
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3069
    ];
3070
3071
    //    Comparison (Boolean) Operators
3072
    //    These operators work on two values, but always return a boolean result
3073
    private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
3074
3075
    //    Operator Precedence
3076
    //    This list includes all valid operators, whether binary (including boolean) or unary (such as %)
3077
    //    Array key is the operator, the value is its precedence
3078
    private static $operatorPrecedence = [
3079
        ':' => 8, //    Range
3080
        '|' => 7, //    Intersect
3081
        '~' => 6, //    Negation
3082
        '%' => 5, //    Percentage
3083
        '^' => 4, //    Exponentiation
3084
        '*' => 3, '/' => 3, //    Multiplication and Division
3085
        '+' => 2, '-' => 2, //    Addition and Subtraction
3086
        '&' => 1, //    Concatenation
3087
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3088
    ];
3089
3090
    // Convert infix to postfix notation
3091 94
    private function _parseFormula($formula, Cell $pCell = null)
3092
    {
3093 94
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
3094
            return false;
3095
        }
3096
3097
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
3098
        //        so we store the parent worksheet so that we can re-attach it when necessary
3099 94
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3100
3101 94
        $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION .
3102 94
                                '|' . self::CALCULATION_REGEXP_CELLREF .
3103 94
                                '|' . self::CALCULATION_REGEXP_NUMBER .
3104 94
                                '|' . self::CALCULATION_REGEXP_STRING .
3105 94
                                '|' . self::CALCULATION_REGEXP_OPENBRACE .
3106 94
                                '|' . self::CALCULATION_REGEXP_NAMEDRANGE .
3107 94
                                '|' . self::CALCULATION_REGEXP_ERROR .
3108 94
                                ')/si';
3109
3110
        //    Start with initialisation
3111 94
        $index = 0;
3112 94
        $stack = new Calculation\Token\Stack();
3113 94
        $output = [];
3114 94
        $expectingOperator = false; //    We use this test in syntax-checking the expression to determine when a
3115
                                                    //        - is a negation or + is a positive operator rather than an operation
3116 94
        $expectingOperand = false; //    We use this test in syntax-checking the expression to determine whether an operand
3117
                                                    //        should be null in a function call
3118
        //    The guts of the lexical parser
3119
        //    Loop through the formula extracting each operator and operand in turn
3120 94
        while (true) {
3121 94
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
3122 94
            if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
3123 34
                $opCharacter .= $formula[++$index];
3124
            }
3125
3126
            //    Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
3127 94
            $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
3128
3129 94
            if ($opCharacter == '-' && !$expectingOperator) {                //    Is it a negation instead of a minus?
3130 1
                $stack->push('Unary Operator', '~'); //    Put a negation on the stack
3131 1
                ++$index; //        and drop the negation symbol
3132 94
            } elseif ($opCharacter == '%' && $expectingOperator) {
3133
                $stack->push('Unary Operator', '%'); //    Put a percentage on the stack
3134
                ++$index;
3135 94
            } elseif ($opCharacter == '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
3136
                ++$index; //    Drop the redundant plus symbol
3137 94
            } elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) {    //    We have to explicitly deny a tilde or pipe, because they are legal
3138
                return $this->raiseFormulaError("Formula Error: Illegal character '~'"); //        on the stack but not in the input expression
3139 94
            } 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...
3140 92 View Code Duplication
                while ($stack->count() > 0 &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3141 92
                    ($o2 = $stack->last()) &&
3142 92
                    isset(self::$operators[$o2['value']]) &&
3143 92
                    @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3144 1
                    $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3145
                }
3146 92
                $stack->push('Binary Operator', $opCharacter); //    Finally put our current operator onto the stack
3147 92
                ++$index;
3148 92
                $expectingOperator = false;
3149 94
            } elseif ($opCharacter == ')' && $expectingOperator) {            //    Are we expecting to close a parenthesis?
3150 24
                $expectingOperand = false;
3151 24 View Code Duplication
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3152 23
                    if ($o2 === null) {
3153
                        return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"');
3154
                    }
3155 23
                    $output[] = $o2;
3156
                }
3157 24
                $d = $stack->last(2);
3158 24
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {    //    Did this parenthesis just close a function?
3159 24
                    $functionName = $matches[1]; //    Get the function name
3160 24
                    $d = $stack->pop();
3161 24
                    $argumentCount = $d['value']; //    See how many arguments there were (argument count is the next value stored on the stack)
3162 24
                    $output[] = $d; //    Dump the argument count on the output
3163 24
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
3164 24
                    if (isset(self::$controlFunctions[$functionName])) {
3165
                        $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
3166
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
0 ignored issues
show
Unused Code introduced by
$functionCall is not used, you could remove the assignment.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
3189 23
                        switch ($argMatch[2]) {
3190 23
                            case '+':
3191 22
                                if ($argumentCount < $argMatch[1]) {
3192
                                    $argumentCountError = true;
3193
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
3194
                                }
3195 22
                                break;
3196 14 View Code Duplication
                            case '-':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3197 12
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
3198
                                    $argumentCountError = true;
3199
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
3200
                                }
3201 12
                                break;
3202 2 View Code Duplication
                            case ',':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3203 2
                                if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
3204
                                    $argumentCountError = true;
3205
                                    $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
3206
                                }
3207 2
                                break;
3208
                        }
3209
                    }
3210 24
                    if ($argumentCountError) {
3211
                        return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected');
0 ignored issues
show
Bug introduced by
The variable $expectedArgumentCountString does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3212
                    }
3213
                }
3214 24
                ++$index;
3215 94
            } elseif ($opCharacter == ',') {            //    Is this the separator for function arguments?
3216 15 View Code Duplication
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3217 13
                    if ($o2 === null) {
3218
                        return $this->raiseFormulaError('Formula Error: Unexpected ,');
3219
                    }
3220 13
                    $output[] = $o2; // pop the argument expression stuff and push onto the output
3221
                }
3222
                //    If we've a comma when we're expecting an operand, then what we actually have is a null operand;
3223
                //        so push a null onto the stack
3224 15
                if (($expectingOperand) || (!$expectingOperator)) {
3225
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3226
                }
3227
                // make sure there was a function
3228 15
                $d = $stack->last(2);
3229 15
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {
3230
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
3231
                }
3232 15
                $d = $stack->pop();
3233 15
                $stack->push($d['type'], ++$d['value'], $d['reference']); // increment the argument count
3234 15
                $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again
3235 15
                $expectingOperator = false;
3236 15
                $expectingOperand = true;
3237 15
                ++$index;
3238 94
            } elseif ($opCharacter == '(' && !$expectingOperator) {
3239 2
                $stack->push('Brace', '(');
3240 2
                ++$index;
3241 94
            } elseif ($isOperandOrFunction && !$expectingOperator) {    // do we now have a function/variable/number?
3242 94
                $expectingOperator = true;
3243 94
                $expectingOperand = false;
3244 94
                $val = $match[1];
3245 94
                $length = strlen($val);
3246
3247 94
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
3248 24
                    $val = preg_replace('/\s/u', '', $val);
3249 24
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
3250 24
                        $stack->push('Function', strtoupper($val));
3251 24
                        $ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
3252 24
                        if ($ax) {
3253 4
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 0);
3254 4
                            $expectingOperator = true;
3255
                        } else {
3256 24
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 1);
3257 24
                            $expectingOperator = false;
3258
                        }
3259 24
                        $stack->push('Brace', '(');
3260
                    } else {    // it's a var w/ implicit multiplication
3261 24
                        $output[] = ['type' => 'Value', 'value' => $matches[1], 'reference' => null];
3262
                    }
3263 94
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
3264
                    //    Watch for this case-change when modifying to allow cell references in different worksheets...
3265
                    //    Should only be applied to the actual cell column, not the worksheet name
3266
3267
                    //    If the last entry on the stack was a : operator, then we have a cell range reference
3268 36
                    $testPrevOp = $stack->last(1);
3269 36
                    if ($testPrevOp['value'] == ':') {
3270
                        //    If we have a worksheet reference, then we're playing with a 3D reference
3271 35
                        if ($matches[2] == '') {
3272
                            //    Otherwise, we 'inherit' the worksheet reference from the start cell reference
3273
                            //    The start of the cell range reference should be the last entry in $output
3274 35
                            $startCellRef = $output[count($output) - 1]['value'];
3275 35
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $startCellRef, $startMatches);
3276 35
                            if ($startMatches[2] > '') {
3277 35
                                $val = $startMatches[2] . '!' . $val;
3278
                            }
3279
                        } else {
3280
                            return $this->raiseFormulaError('3D Range references are not yet supported');
3281
                        }
3282
                    }
3283
3284 36
                    $output[] = ['type' => 'Cell Reference', 'value' => $val, 'reference' => $val];
3285
                } else {    // it's a variable, constant, string, number or boolean
3286
                    //    If the last entry on the stack was a : operator, then we may have a row or column range reference
3287 77
                    $testPrevOp = $stack->last(1);
3288 77
                    if ($testPrevOp['value'] == ':') {
3289
                        $startRowColRef = $output[count($output) - 1]['value'];
3290
                        $rangeWS1 = '';
3291
                        if (strpos('!', $startRowColRef) !== false) {
3292
                            list($rangeWS1, $startRowColRef) = explode('!', $startRowColRef);
3293
                        }
3294
                        if ($rangeWS1 != '') {
3295
                            $rangeWS1 .= '!';
3296
                        }
3297
                        $rangeWS2 = $rangeWS1;
3298
                        if (strpos('!', $val) !== false) {
3299
                            list($rangeWS2, $val) = explode('!', $val);
3300
                        }
3301
                        if ($rangeWS2 != '') {
3302
                            $rangeWS2 .= '!';
3303
                        }
3304
                        if ((is_int($startRowColRef)) && (ctype_digit($val)) &&
3305
                            ($startRowColRef <= 1048576) && ($val <= 1048576)) {
3306
                            //    Row range
3307
                            $endRowColRef = ($pCellParent !== null) ? $pCellParent->getHighestColumn() : 'XFD'; //    Max 16,384 columns for Excel2007
3308
                            $output[count($output) - 1]['value'] = $rangeWS1 . 'A' . $startRowColRef;
3309
                            $val = $rangeWS2 . $endRowColRef . $val;
3310
                        } elseif ((ctype_alpha($startRowColRef)) && (ctype_alpha($val)) &&
3311
                            (strlen($startRowColRef) <= 3) && (strlen($val) <= 3)) {
3312
                            //    Column range
3313
                            $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...
3314
                            $output[count($output) - 1]['value'] = $rangeWS1 . strtoupper($startRowColRef) . '1';
3315
                            $val = $rangeWS2 . $val . $endRowColRef;
3316
                        }
3317
                    }
3318
3319 77
                    $localeConstant = false;
3320 77
                    if ($opCharacter == '"') {
3321
                        //    UnEscape any quotes within the string
3322 48
                        $val = self::wrapResult(str_replace('""', '"', self::unwrapResult($val)));
3323 45
                    } elseif (is_numeric($val)) {
3324 43
                        if ((strpos($val, '.') !== false) || (stripos($val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
3325 19
                            $val = (float) $val;
3326
                        } else {
3327 43
                            $val = (int) $val;
3328
                        }
3329 5
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
3330 1
                        $excelConstant = trim(strtoupper($val));
3331 1
                        $val = self::$excelConstants[$excelConstant];
3332 4
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
3333
                        $val = self::$excelConstants[$localeConstant];
3334
                    }
3335 77
                    $details = ['type' => 'Value', 'value' => $val, 'reference' => null];
3336 77
                    if ($localeConstant) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $localeConstant of type integer|false is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
3337
                        $details['localeValue'] = $localeConstant;
3338
                    }
3339 77
                    $output[] = $details;
3340
                }
3341 94
                $index += $length;
3342
            } elseif ($opCharacter == '$') {    // absolute row or column range
3343
                ++$index;
3344
            } elseif ($opCharacter == ')') {    // miscellaneous error checking
3345
                if ($expectingOperand) {
3346
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3347
                    $expectingOperand = false;
3348
                    $expectingOperator = true;
3349
                } else {
3350
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
3351
                }
3352
            } elseif (isset(self::$operators[$opCharacter]) && !$expectingOperator) {
3353
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
3354
            } else {    // I don't even want to know what you did to get here
3355
                return $this->raiseFormulaError('Formula Error: An unexpected error occured');
3356
            }
3357
            //    Test for end of formula string
3358 94
            if ($index == strlen($formula)) {
3359
                //    Did we end with an operator?.
3360
                //    Only valid for the % unary operator
3361 94
                if ((isset(self::$operators[$opCharacter])) && ($opCharacter != '%')) {
3362
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
3363
                }
3364 94
                break;
3365
            }
3366
            //    Ignore white space
3367 93
            while (($formula[$index] == "\n") || ($formula[$index] == "\r")) {
3368
                ++$index;
3369
            }
3370 93
            if ($formula[$index] == ' ') {
3371 53
                while ($formula[$index] == ' ') {
3372 53
                    ++$index;
3373
                }
3374
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
3375
                //        Cell References) then we have an INTERSECTION operator
3376 53
                if (($expectingOperator) && (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
3377 53
                    ($output[count($output) - 1]['type'] == 'Cell Reference')) {
3378 View Code Duplication
                    while ($stack->count() > 0 &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3379
                        ($o2 = $stack->last()) &&
3380
                        isset(self::$operators[$o2['value']]) &&
3381
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3382
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3383
                    }
3384
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
3385
                    $expectingOperator = false;
3386
                }
3387
            }
3388
        }
3389
3390 94 View Code Duplication
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3391 88
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
3392
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
3393
            }
3394 88
            $output[] = $op;
3395
        }
3396
3397 94
        return $output;
3398
    }
3399
3400 92
    private static function dataTestReference(&$operandData)
3401
    {
3402 92
        $operand = $operandData['value'];
3403 92
        if (($operandData['reference'] === null) && (is_array($operand))) {
3404 1
            $rKeys = array_keys($operand);
3405 1
            $rowKey = array_shift($rKeys);
3406 1
            $cKeys = array_keys(array_keys($operand[$rowKey]));
3407 1
            $colKey = array_shift($cKeys);
3408 1
            if (ctype_upper($colKey)) {
3409
                $operandData['reference'] = $colKey . $rowKey;
3410
            }
3411
        }
3412
3413 92
        return $operand;
3414
    }
3415
3416
    // evaluate postfix notation
3417
3418
    /**
3419
     * @param string $cellID
3420
     * @param mixed $tokens
3421
     */
3422 94
    private function processTokenStack($tokens, $cellID = null, Cell $pCell = null)
3423
    {
3424 94
        if ($tokens == false) {
3425
            return false;
3426
        }
3427
3428
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
3429
        //        so we store the parent cell collection so that we can re-attach it when necessary
3430 94
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
3431 94
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
3432 94
        $stack = new Calculation\Token\Stack();
3433
3434
        //    Loop through each token in turn
3435 94
        foreach ($tokens as $tokenData) {
3436 94
            $token = $tokenData['value'];
3437
            // 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
3438 94
            if (isset(self::$binaryOperators[$token])) {
3439
                //    We must have two operands, error if we don't
3440 92
                if (($operand2Data = $stack->pop()) === null) {
3441
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3442
                }
3443 92
                if (($operand1Data = $stack->pop()) === null) {
3444
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3445
                }
3446
3447 92
                $operand1 = self::dataTestReference($operand1Data);
3448 92
                $operand2 = self::dataTestReference($operand2Data);
3449
3450
                //    Log what we're doing
3451 92
                if ($token == ':') {
3452 35
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
3453
                } else {
3454 76
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
3455
                }
3456
3457
                //    Process the operation in the appropriate manner
3458
                switch ($token) {
3459
                    //    Comparison (Boolean) Operators
3460 92
                    case '>':            //    Greater than
3461 78
                    case '<':            //    Less than
3462 71
                    case '>=':            //    Greater than or Equal to
3463 63
                    case '<=':            //    Less than or Equal to
3464 56
                    case '=':            //    Equality
3465 47
                    case '<>':            //    Inequality
3466 64
                        $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
3467 64
                        break;
3468
                    //    Binary Operators
3469 40
                    case ':':            //    Range
3470 35
                        $sheet1 = $sheet2 = '';
3471 35
                        if (strpos($operand1Data['reference'], '!') !== false) {
3472 35
                            list($sheet1, $operand1Data['reference']) = explode('!', $operand1Data['reference']);
3473
                        } else {
3474
                            $sheet1 = ($pCellParent !== null) ? $pCellWorksheet->getTitle() : '';
3475
                        }
3476 35
                        if (strpos($operand2Data['reference'], '!') !== false) {
3477 35
                            list($sheet2, $operand2Data['reference']) = explode('!', $operand2Data['reference']);
3478
                        } else {
3479 9
                            $sheet2 = $sheet1;
3480
                        }
3481 35
                        if ($sheet1 == $sheet2) {
3482 35 View Code Duplication
                            if ($operand1Data['reference'] === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3483
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
3484
                                    $operand1Data['reference'] = $pCell->getColumn() . $operand1Data['value'];
0 ignored issues
show
Bug introduced by
It seems like $pCell is not always an object, but can also be of type null. Maybe add an additional type check?

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

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
3485
                                } elseif (trim($operand1Data['reference']) == '') {
3486
                                    $operand1Data['reference'] = $pCell->getCoordinate();
3487
                                } else {
3488
                                    $operand1Data['reference'] = $operand1Data['value'] . $pCell->getRow();
3489
                                }
3490
                            }
3491 35 View Code Duplication
                            if ($operand2Data['reference'] === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3492
                                if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
3493
                                    $operand2Data['reference'] = $pCell->getColumn() . $operand2Data['value'];
3494
                                } elseif (trim($operand2Data['reference']) == '') {
3495
                                    $operand2Data['reference'] = $pCell->getCoordinate();
3496
                                } else {
3497
                                    $operand2Data['reference'] = $operand2Data['value'] . $pCell->getRow();
3498
                                }
3499
                            }
3500
3501 35
                            $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
3502 35
                            $oCol = $oRow = [];
3503 35
                            foreach ($oData as $oDatum) {
3504 35
                                $oCR = Cell::coordinateFromString($oDatum);
3505 35
                                $oCol[] = Cell::columnIndexFromString($oCR[0]) - 1;
3506 35
                                $oRow[] = $oCR[1];
3507
                            }
3508 35
                            $cellRef = Cell::stringFromColumnIndex(min($oCol)) . min($oRow) . ':' . Cell::stringFromColumnIndex(max($oCol)) . max($oRow);
3509 35 View Code Duplication
                            if ($pCellParent !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3510 35
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
3511
                            } else {
3512
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3513
                            }
3514 35
                            $stack->push('Cell Reference', $cellValue, $cellRef);
3515
                        } else {
3516
                            $stack->push('Error', Calculation\Functions::REF(), null);
3517
                        }
3518 35
                        break;
3519 24
                    case '+':            //    Addition
3520 20
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'plusEquals', $stack);
3521 20
                        break;
3522 22
                    case '-':            //    Subtraction
3523 6
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'minusEquals', $stack);
3524 6
                        break;
3525 20
                    case '*':            //    Multiplication
3526 18
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'arrayTimesEquals', $stack);
3527 18
                        break;
3528 6
                    case '/':            //    Division
3529 3
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'arrayRightDivide', $stack);
3530 3
                        break;
3531 6
                    case '^':            //    Exponential
3532
                        $this->executeNumericBinaryOperation($cellID, $operand1, $operand2, $token, 'power', $stack);
3533
                        break;
3534 6
                    case '&':            //    Concatenation
3535
                        //    If either of the operands is a matrix, we need to treat them both as matrices
3536
                        //        (converting the other operand to a matrix if need be); then perform the required
3537
                        //        matrix operation
3538 6
                        if (is_bool($operand1)) {
3539
                            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3540
                        }
3541 6
                        if (is_bool($operand2)) {
3542
                            $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3543
                        }
3544 6
                        if ((is_array($operand1)) || (is_array($operand2))) {
3545
                            //    Ensure that both operands are arrays/matrices
3546 5
                            self::checkMatrixOperands($operand1, $operand2, 2);
3547
                            try {
3548
                                //    Convert operand 1 from a PHP array to a matrix
3549 5
                                $matrix = new Shared\JAMA\Matrix($operand1);
3550
                                //    Perform the required operation against the operand 1 matrix, passing in operand 2
3551 5
                                $matrixResult = $matrix->concat($operand2);
3552 5
                                $result = $matrixResult->getArray();
3553
                            } catch (Exception $ex) {
3554
                                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3555 5
                                $result = '#VALUE!';
3556
                            }
3557
                        } else {
3558 1
                            $result = '"' . str_replace('""', '"', self::unwrapResult($operand1) . self::unwrapResult($operand2)) . '"';
3559
                        }
3560 6
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3561 6
                        $stack->push('Value', $result);
3562 6
                        break;
3563
                    case '|':            //    Intersect
3564
                        $rowIntersect = array_intersect_key($operand1, $operand2);
3565
                        $cellIntersect = $oCol = $oRow = [];
3566
                        foreach (array_keys($rowIntersect) as $row) {
3567
                            $oRow[] = $row;
3568
                            foreach ($rowIntersect[$row] as $col => $data) {
3569
                                $oCol[] = Cell::columnIndexFromString($col) - 1;
3570
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
3571
                            }
3572
                        }
3573
                        $cellRef = Cell::stringFromColumnIndex(min($oCol)) . min($oRow) . ':' . Cell::stringFromColumnIndex(max($oCol)) . max($oRow);
3574
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
3575
                        $stack->push('Value', $cellIntersect, $cellRef);
3576 92
                        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 94
            } elseif (($token === '~') || ($token === '%')) {
3581 1
                if (($arg = $stack->pop()) === null) {
3582
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3583
                }
3584 1
                $arg = $arg['value'];
3585 1
                if ($token === '~') {
3586 1
                    $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg));
3587 1
                    $multiplier = -1;
3588
                } else {
3589
                    $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg));
3590
                    $multiplier = 0.01;
3591
                }
3592 1
                if (is_array($arg)) {
3593
                    self::checkMatrixOperands($arg, $multiplier, 2);
3594
                    try {
3595
                        $matrix1 = new Shared\JAMA\Matrix($arg);
3596
                        $matrixResult = $matrix1->arrayTimesEquals($multiplier);
3597
                        $result = $matrixResult->getArray();
3598
                    } catch (Exception $ex) {
3599
                        $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3600
                        $result = '#VALUE!';
3601
                    }
3602
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3603
                    $stack->push('Value', $result);
3604
                } else {
3605 1
                    $this->executeNumericBinaryOperation($cellID, $multiplier, $arg, '*', 'arrayTimesEquals', $stack);
3606
                }
3607 94
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token, $matches)) {
3608 36
                $cellRef = null;
3609 36
                if (isset($matches[8])) {
3610
                    if ($pCell === null) {
3611
                        //                        We can't access the range, so return a REF error
3612
                        $cellValue = Calculation\Functions::REF();
3613
                    } else {
3614
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
3615
                        if ($matches[2] > '') {
3616
                            $matches[2] = trim($matches[2], "\"'");
3617 View Code Duplication
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3618
                                //    It's a Reference to an external spreadsheet (not currently supported)
3619
                                return $this->raiseFormulaError('Unable to access External Workbook');
3620
                            }
3621
                            $matches[2] = trim($matches[2], "\"'");
3622
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]);
3623 View Code Duplication
                            if ($pCellParent !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3624
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3625
                            } else {
3626
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3627
                            }
3628
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3629
                        } else {
3630
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet');
3631
                            if ($pCellParent !== null) {
3632
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
3633
                            } else {
3634
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3635
                            }
3636
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
3637
                        }
3638
                    }
3639
                } else {
3640 36
                    if ($pCell === null) {
3641
                        //                        We can't access the cell, so return a REF error
3642
                        $cellValue = Calculation\Functions::REF();
3643
                    } else {
3644 36
                        $cellRef = $matches[6] . $matches[7];
3645 36
                        if ($matches[2] > '') {
3646 13
                            $matches[2] = trim($matches[2], "\"'");
3647 13 View Code Duplication
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3648
                                //    It's a Reference to an external spreadsheet (not currently supported)
3649
                                return $this->raiseFormulaError('Unable to access External Workbook');
3650
                            }
3651 13
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]);
3652 13
                            if ($pCellParent !== null) {
3653 13
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
3654 13
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
3655 13
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3656 13
                                    $pCell->attach($pCellParent);
3657
                                } else {
3658 13
                                    $cellValue = null;
3659
                                }
3660
                            } else {
3661
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3662
                            }
3663 13
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3664
                        } else {
3665 24
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet');
3666 24
                            if ($pCellParent->isDataSet($cellRef)) {
3667 24
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
3668 24
                                $pCell->attach($pCellParent);
0 ignored issues
show
Bug introduced by
It seems like $pCellParent defined by $pCell !== null ? $pCell->getParent() : null on line 3431 can be null; however, PhpOffice\PhpSpreadsheet\Cell::attach() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
3774 4
                    $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
3775 4
                    $stack->push('Named Range', $cellValue, $namedRange);
3776
                } else {
3777 94
                    return $this->raiseFormulaError("undefined variable '$token'");
3778
                }
3779
            }
3780
        }
3781
        // when we're out of tokens, the stack should have a single element, the final result
3782 94
        if ($stack->count() != 1) {
3783
            return $this->raiseFormulaError('internal error');
3784
        }
3785 94
        $output = $stack->pop();
3786 94
        $output = $output['value'];
3787
3788
//        if ((is_array($output)) && (self::$returnArrayAsType != self::RETURN_ARRAY_AS_ARRAY)) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% 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...
3789
//            return array_shift(Calculation\Functions::flattenArray($output));
3790
//        }
3791 94
        return $output;
3792
    }
3793
3794 23
    private function validateBinaryOperand($cellID, &$operand, &$stack)
0 ignored issues
show
Unused Code introduced by
The parameter $cellID is not used and could be removed.

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

Loading history...
3795
    {
3796 23
        if (is_array($operand)) {
3797 19
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
3798
                do {
3799 19
                    $operand = array_pop($operand);
3800 19
                } while (is_array($operand));
3801
            }
3802
        }
3803
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
3804 23
        if (is_string($operand)) {
3805
            //    We only need special validations for the operand if it is a string
3806
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
3807 1
            if ($operand > '' && $operand[0] == '"') {
3808
                $operand = self::unwrapResult($operand);
3809
            }
3810
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
3811 1
            if (!is_numeric($operand)) {
3812
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
3813 1
                if ($operand > '' && $operand[0] == '#') {
3814
                    $stack->push('Value', $operand);
3815
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand));
3816
3817
                    return false;
3818 1
                } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
3819
                    //    If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
3820 1
                    $stack->push('Value', '#VALUE!');
3821 1
                    $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!'));
3822
3823 1
                    return false;
3824
                }
3825
            }
3826
        }
3827
3828
        //    return a true if the value of the operand is one that we can use in normal binary operations
3829 23
        return true;
3830
    }
3831
3832 64
    private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, &$stack, $recursingArrays = false)
3833
    {
3834
        //    If we're dealing with matrix operations, we want a matrix result
3835 64
        if ((is_array($operand1)) || (is_array($operand2))) {
3836 12
            $result = [];
3837 12
            if ((is_array($operand1)) && (!is_array($operand2))) {
3838 12 View Code Duplication
                foreach ($operand1 as $x => $operandData) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3839 12
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2));
3840 12
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack);
3841 12
                    $r = $stack->pop();
3842 12
                    $result[$x] = $r['value'];
3843
                }
3844
            } elseif ((!is_array($operand1)) && (is_array($operand2))) {
3845 View Code Duplication
                foreach ($operand2 as $x => $operandData) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3846
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData));
3847
                    $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack);
3848
                    $r = $stack->pop();
3849
                    $result[$x] = $r['value'];
3850
                }
3851
            } else {
3852
                if (!$recursingArrays) {
3853
                    self::checkMatrixOperands($operand1, $operand2, 2);
3854
                }
3855
                foreach ($operand1 as $x => $operandData) {
3856
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x]));
3857
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true);
3858
                    $r = $stack->pop();
3859
                    $result[$x] = $r['value'];
3860
                }
3861
            }
3862
            //    Log the result details
3863 12
            $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result));
3864
            //    And push the result onto the stack
3865 12
            $stack->push('Array', $result);
3866
3867 12
            return true;
3868
        }
3869
3870
        //    Simple validate the two operands if they are string values
3871 64
        if (is_string($operand1) && $operand1 > '' && $operand1[0] == '"') {
3872 33
            $operand1 = self::unwrapResult($operand1);
3873
        }
3874 64
        if (is_string($operand2) && $operand2 > '' && $operand2[0] == '"') {
3875 45
            $operand2 = self::unwrapResult($operand2);
3876
        }
3877
3878
        // Use case insensitive comparaison if not OpenOffice mode
3879 64
        if (Calculation\Functions::getCompatibilityMode() != Calculation\Functions::COMPATIBILITY_OPENOFFICE) {
3880 64
            if (is_string($operand1)) {
3881 33
                $operand1 = strtoupper($operand1);
3882
            }
3883 64
            if (is_string($operand2)) {
3884 45
                $operand2 = strtoupper($operand2);
3885
            }
3886
        }
3887
3888 64
        $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Calculation\Functions::getCompatibilityMode() == Calculation\Functions::COMPATIBILITY_OPENOFFICE;
3889
3890
        //    execute the necessary operation
3891
        switch ($operation) {
3892
            //    Greater than
3893 64 View Code Duplication
            case '>':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3894 14
                if ($useLowercaseFirstComparison) {
3895 9
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0;
3896
                } else {
3897 14
                    $result = ($operand1 > $operand2);
3898
                }
3899 14
                break;
3900
            //    Less than
3901 50 View Code Duplication
            case '<':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3902 7
                if ($useLowercaseFirstComparison) {
3903 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0;
3904
                } else {
3905 7
                    $result = ($operand1 < $operand2);
3906
                }
3907 7
                break;
3908
            //    Equality
3909 43 View Code Duplication
            case '=':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3910 9
                if (is_numeric($operand1) && is_numeric($operand2)) {
3911 3
                    $result = (abs($operand1 - $operand2) < $this->delta);
3912
                } else {
3913 6
                    $result = strcmp($operand1, $operand2) == 0;
3914
                }
3915 9
                break;
3916
            //    Greater than or equal
3917 34
            case '>=':
3918 8
                if (is_numeric($operand1) && is_numeric($operand2)) {
3919 4
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2));
3920 4
                } elseif ($useLowercaseFirstComparison) {
3921 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0;
3922
                } else {
3923 4
                    $result = strcmp($operand1, $operand2) >= 0;
3924
                }
3925 8
                break;
3926
            //    Less than or equal
3927 26
            case '<=':
3928 7
                if (is_numeric($operand1) && is_numeric($operand2)) {
3929 3
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2));
3930 4
                } elseif ($useLowercaseFirstComparison) {
3931 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0;
3932
                } else {
3933 4
                    $result = strcmp($operand1, $operand2) <= 0;
3934
                }
3935 7
                break;
3936
            //    Inequality
3937 19 View Code Duplication
            case '<>':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3938 19
                if (is_numeric($operand1) && is_numeric($operand2)) {
3939 3
                    $result = (abs($operand1 - $operand2) > 1E-14);
3940
                } else {
3941 16
                    $result = strcmp($operand1, $operand2) != 0;
3942
                }
3943 19
                break;
3944
        }
3945
3946
        //    Log the result details
3947 64
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
4046
        //    And push the result onto the stack
4047 23
        $stack->push('Value', $result);
4048
4049 23
        return true;
4050
    }
4051
4052
    // trigger an error, but nicely, if need be
4053
    protected function raiseFormulaError($errorMessage)
4054
    {
4055
        $this->formulaError = $errorMessage;
4056
        $this->cyclicReferenceStack->clear();
0 ignored issues
show
Bug introduced by
The method clear cannot be called on $this->cyclicReferenceStack (of type array).

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

Loading history...
4057
        if (!$this->suppressFormulaErrors) {
4058
            throw new Calculation\Exception($errorMessage);
4059
        }
4060
        trigger_error($errorMessage, E_USER_ERROR);
4061
    }
4062
4063
    /**
4064
     * Extract range values.
4065
     *
4066
     * @param string &$pRange String based range representation
4067
     * @param Worksheet $pSheet Worksheet
4068
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4069
     *
4070
     * @throws Calculation\Exception
4071
     *
4072
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4073
     */
4074 36
    public function extractCellRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4075
    {
4076
        // Return value
4077 36
        $returnValue = [];
4078
4079 36
        if ($pSheet !== null) {
4080 36
            $pSheetName = $pSheet->getTitle();
4081 36 View Code Duplication
            if (strpos($pRange, '!') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4082
                list($pSheetName, $pRange) = Worksheet::extractSheetTitle($pRange, true);
4083
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4084
            }
4085
4086
            // Extract range
4087 36
            $aReferences = Cell::extractAllCellReferencesInRange($pRange);
4088 36
            $pRange = $pSheetName . '!' . $pRange;
4089 36
            if (!isset($aReferences[1])) {
4090
                //    Single cell in range
4091 36
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
0 ignored issues
show
Bug introduced by
The variable $currentRow does not exist. Did you forget to declare it?

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

Loading history...
4092 36 View Code Duplication
                if ($pSheet->cellExists($aReferences[0])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4093 36
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4094
                } else {
4095 36
                    $returnValue[$currentRow][$currentCol] = null;
4096
                }
4097
            } else {
4098
                // Extract cell data for all cells in the range
4099 35
                foreach ($aReferences as $reference) {
4100
                    // Extract range
4101 35
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
4102 35 View Code Duplication
                    if ($pSheet->cellExists($reference)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4103 35
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4104
                    } else {
4105 35
                        $returnValue[$currentRow][$currentCol] = null;
4106
                    }
4107
                }
4108
            }
4109
        }
4110
4111 36
        return $returnValue;
4112
    }
4113
4114
    /**
4115
     * Extract range values.
4116
     *
4117
     * @param string &$pRange String based range representation
4118
     * @param Worksheet $pSheet Worksheet
4119
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4120
     *
4121
     * @throws Calculation\Exception
4122
     *
4123
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4124
     */
4125 4
    public function extractNamedRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4126
    {
4127
        // Return value
4128 4
        $returnValue = [];
4129
4130 4
        if ($pSheet !== null) {
4131 4
            $pSheetName = $pSheet->getTitle();
4132 4 View Code Duplication
            if (strpos($pRange, '!') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4133
                list($pSheetName, $pRange) = Worksheet::extractSheetTitle($pRange, true);
4134
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4135
            }
4136
4137
            // Named range?
4138 4
            $namedRange = NamedRange::resolveRange($pRange, $pSheet);
4139 4
            if ($namedRange !== null) {
4140 2
                $pSheet = $namedRange->getWorksheet();
4141 2
                $pRange = $namedRange->getRange();
4142 2
                $splitRange = Cell::splitRange($pRange);
4143
                //    Convert row and column references
4144 2
                if (ctype_alpha($splitRange[0][0])) {
4145
                    $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
4146 2
                } elseif (ctype_digit($splitRange[0][0])) {
4147 2
                    $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
4148
                }
4149
            } else {
4150 2
                return Calculation\Functions::REF();
4151
            }
4152
4153
            // Extract range
4154 2
            $aReferences = Cell::extractAllCellReferencesInRange($pRange);
4155 2
            if (!isset($aReferences[1])) {
4156
                //    Single cell (or single column or row) in range
4157 1
                list($currentCol, $currentRow) = Cell::coordinateFromString($aReferences[0]);
4158 1 View Code Duplication
                if ($pSheet->cellExists($aReferences[0])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4159 1
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4160
                } else {
4161 1
                    $returnValue[$currentRow][$currentCol] = null;
4162
                }
4163
            } else {
4164
                // Extract cell data for all cells in the range
4165 1
                foreach ($aReferences as $reference) {
4166
                    // Extract range
4167 1
                    list($currentCol, $currentRow) = Cell::coordinateFromString($reference);
4168 1 View Code Duplication
                    if ($pSheet->cellExists($reference)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4169 1
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4170
                    } else {
4171 1
                        $returnValue[$currentRow][$currentCol] = null;
4172
                    }
4173
                }
4174
            }
4175
        }
4176
4177 2
        return $returnValue;
4178
    }
4179
4180
    /**
4181
     * Is a specific function implemented?
4182
     *
4183
     * @param string $pFunction Function Name
4184
     *
4185
     * @return bool
4186
     */
4187 3
    public function isImplemented($pFunction)
4188
    {
4189 3
        $pFunction = strtoupper($pFunction);
4190 3
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
4191
4192 3
        return !$notImplemented;
4193
    }
4194
4195
    /**
4196
     * Get a list of all implemented functions as an array of function objects.
4197
     *
4198
     * @return array of Calculation\Category
4199
     */
4200
    public function getFunctions()
4201
    {
4202
        return self::$phpSpreadsheetFunctions;
4203
    }
4204
4205
    /**
4206
     * Get a list of implemented Excel function names.
4207
     *
4208
     * @return array
4209
     */
4210 2
    public function getImplementedFunctionNames()
4211
    {
4212 2
        $returnValue = [];
4213 2
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
4214 2
            if ($this->isImplemented($functionName)) {
4215 2
                $returnValue[] = $functionName;
4216
            }
4217
        }
4218
4219 2
        return $returnValue;
4220
    }
4221
}
4222