Failed Conditions
Push — master ( 27d83b...a2771e )
by Adrien
35:04
created

Calculation::calculateCellValue()   C

Complexity

Conditions 15
Paths 37

Size

Total Lines 67
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 29.9246

Importance

Changes 0
Metric Value
cc 15
eloc 42
nc 37
nop 2
dl 0
loc 67
ccs 25
cts 42
cp 0.5951
crap 29.9246
rs 5.7992
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

Loading history...
166
    ];
167
168
    /**
169
     * Locale-specific argument separator for function arguments.
170
     *
171
     * @var string
172
     */
173
    private static $localeArgumentSeparator = ',';
174
175
    private static $localeFunctions = [];
176
177
    /**
178
     * Locale-specific translations for Excel constants (True, False and Null).
179
     *
180
     * @var string[]
181
     */
182
    public static $localeBoolean = [
183
        'TRUE' => 'TRUE',
184
        'FALSE' => 'FALSE',
185
        'NULL' => 'NULL',
186
    ];
187
188
    /**
189
     * Excel constant string translations to their PHP equivalents
190
     * Constant conversion from text name/value to actual (datatyped) value.
191
     *
192
     * @var string[]
193
     */
194
    private static $excelConstants = [
195
        'TRUE' => true,
196
        'FALSE' => false,
197
        'NULL' => null,
198
    ];
199
200
    // PhpSpreadsheet functions
201
    private static $phpSpreadsheetFunctions = [
202
        'ABS' => [
203
            'category' => Category::CATEGORY_MATH_AND_TRIG,
204
            'functionCall' => 'abs',
205
            'argumentCount' => '1',
206
        ],
207
        'ACCRINT' => [
208
            'category' => Category::CATEGORY_FINANCIAL,
209
            'functionCall' => [Financial::class, 'ACCRINT'],
210
            'argumentCount' => '4-7',
211
        ],
212
        'ACCRINTM' => [
213
            'category' => Category::CATEGORY_FINANCIAL,
214
            'functionCall' => [Financial::class, 'ACCRINTM'],
215
            'argumentCount' => '3-5',
216
        ],
217
        'ACOS' => [
218
            'category' => Category::CATEGORY_MATH_AND_TRIG,
219
            'functionCall' => 'acos',
220
            'argumentCount' => '1',
221
        ],
222
        'ACOSH' => [
223
            'category' => Category::CATEGORY_MATH_AND_TRIG,
224
            'functionCall' => 'acosh',
225
            'argumentCount' => '1',
226
        ],
227
        'ADDRESS' => [
228
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
229
            'functionCall' => [LookupRef::class, 'cellAddress'],
230
            'argumentCount' => '2-5',
231
        ],
232
        'AMORDEGRC' => [
233
            'category' => Category::CATEGORY_FINANCIAL,
234
            'functionCall' => [Financial::class, 'AMORDEGRC'],
235
            'argumentCount' => '6,7',
236
        ],
237
        'AMORLINC' => [
238
            'category' => Category::CATEGORY_FINANCIAL,
239
            'functionCall' => [Financial::class, 'AMORLINC'],
240
            'argumentCount' => '6,7',
241
        ],
242
        'AND' => [
243
            'category' => Category::CATEGORY_LOGICAL,
244
            'functionCall' => [Logical::class, 'logicalAnd'],
245
            'argumentCount' => '1+',
246
        ],
247
        'AREAS' => [
248
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
249
            'functionCall' => [Functions::class, 'DUMMY'],
250
            'argumentCount' => '1',
251
        ],
252
        'ASC' => [
253
            'category' => Category::CATEGORY_TEXT_AND_DATA,
254
            'functionCall' => [Functions::class, 'DUMMY'],
255
            'argumentCount' => '1',
256
        ],
257
        'ASIN' => [
258
            'category' => Category::CATEGORY_MATH_AND_TRIG,
259
            'functionCall' => 'asin',
260
            'argumentCount' => '1',
261
        ],
262
        'ASINH' => [
263
            'category' => Category::CATEGORY_MATH_AND_TRIG,
264
            'functionCall' => 'asinh',
265
            'argumentCount' => '1',
266
        ],
267
        'ATAN' => [
268
            'category' => Category::CATEGORY_MATH_AND_TRIG,
269
            'functionCall' => 'atan',
270
            'argumentCount' => '1',
271
        ],
272
        'ATAN2' => [
273
            'category' => Category::CATEGORY_MATH_AND_TRIG,
274
            'functionCall' => [MathTrig::class, 'ATAN2'],
275
            'argumentCount' => '2',
276
        ],
277
        'ATANH' => [
278
            'category' => Category::CATEGORY_MATH_AND_TRIG,
279
            'functionCall' => 'atanh',
280
            'argumentCount' => '1',
281
        ],
282
        'AVEDEV' => [
283
            'category' => Category::CATEGORY_STATISTICAL,
284
            'functionCall' => [Statistical::class, 'AVEDEV'],
285
            'argumentCount' => '1+',
286
        ],
287
        'AVERAGE' => [
288
            'category' => Category::CATEGORY_STATISTICAL,
289
            'functionCall' => [Statistical::class, 'AVERAGE'],
290
            'argumentCount' => '1+',
291
        ],
292
        'AVERAGEA' => [
293
            'category' => Category::CATEGORY_STATISTICAL,
294
            'functionCall' => [Statistical::class, 'AVERAGEA'],
295
            'argumentCount' => '1+',
296
        ],
297
        'AVERAGEIF' => [
298
            'category' => Category::CATEGORY_STATISTICAL,
299
            'functionCall' => [Statistical::class, 'AVERAGEIF'],
300
            'argumentCount' => '2,3',
301
        ],
302
        'AVERAGEIFS' => [
303
            'category' => Category::CATEGORY_STATISTICAL,
304
            'functionCall' => [Functions::class, 'DUMMY'],
305
            'argumentCount' => '3+',
306
        ],
307
        'BAHTTEXT' => [
308
            'category' => Category::CATEGORY_TEXT_AND_DATA,
309
            'functionCall' => [Functions::class, 'DUMMY'],
310
            'argumentCount' => '1',
311
        ],
312
        'BESSELI' => [
313
            'category' => Category::CATEGORY_ENGINEERING,
314
            'functionCall' => [Engineering::class, 'BESSELI'],
315
            'argumentCount' => '2',
316
        ],
317
        'BESSELJ' => [
318
            'category' => Category::CATEGORY_ENGINEERING,
319
            'functionCall' => [Engineering::class, 'BESSELJ'],
320
            'argumentCount' => '2',
321
        ],
322
        'BESSELK' => [
323
            'category' => Category::CATEGORY_ENGINEERING,
324
            'functionCall' => [Engineering::class, 'BESSELK'],
325
            'argumentCount' => '2',
326
        ],
327
        'BESSELY' => [
328
            'category' => Category::CATEGORY_ENGINEERING,
329
            'functionCall' => [Engineering::class, 'BESSELY'],
330
            'argumentCount' => '2',
331
        ],
332
        'BETADIST' => [
333
            'category' => Category::CATEGORY_STATISTICAL,
334
            'functionCall' => [Statistical::class, 'BETADIST'],
335
            'argumentCount' => '3-5',
336
        ],
337
        'BETAINV' => [
338
            'category' => Category::CATEGORY_STATISTICAL,
339
            'functionCall' => [Statistical::class, 'BETAINV'],
340
            'argumentCount' => '3-5',
341
        ],
342
        'BIN2DEC' => [
343
            'category' => Category::CATEGORY_ENGINEERING,
344
            'functionCall' => [Engineering::class, 'BINTODEC'],
345
            'argumentCount' => '1',
346
        ],
347
        'BIN2HEX' => [
348
            'category' => Category::CATEGORY_ENGINEERING,
349
            'functionCall' => [Engineering::class, 'BINTOHEX'],
350
            'argumentCount' => '1,2',
351
        ],
352
        'BIN2OCT' => [
353
            'category' => Category::CATEGORY_ENGINEERING,
354
            'functionCall' => [Engineering::class, 'BINTOOCT'],
355
            'argumentCount' => '1,2',
356
        ],
357
        'BINOMDIST' => [
358
            'category' => Category::CATEGORY_STATISTICAL,
359
            'functionCall' => [Statistical::class, 'BINOMDIST'],
360
            'argumentCount' => '4',
361
        ],
362
        'CEILING' => [
363
            'category' => Category::CATEGORY_MATH_AND_TRIG,
364
            'functionCall' => [MathTrig::class, 'CEILING'],
365
            'argumentCount' => '2',
366
        ],
367
        'CELL' => [
368
            'category' => Category::CATEGORY_INFORMATION,
369
            'functionCall' => [Functions::class, 'DUMMY'],
370
            'argumentCount' => '1,2',
371
        ],
372
        'CHAR' => [
373
            'category' => Category::CATEGORY_TEXT_AND_DATA,
374
            'functionCall' => [TextData::class, 'CHARACTER'],
375
            'argumentCount' => '1',
376
        ],
377
        'CHIDIST' => [
378
            'category' => Category::CATEGORY_STATISTICAL,
379
            'functionCall' => [Statistical::class, 'CHIDIST'],
380
            'argumentCount' => '2',
381
        ],
382
        'CHIINV' => [
383
            'category' => Category::CATEGORY_STATISTICAL,
384
            'functionCall' => [Statistical::class, 'CHIINV'],
385
            'argumentCount' => '2',
386
        ],
387
        'CHITEST' => [
388
            'category' => Category::CATEGORY_STATISTICAL,
389
            'functionCall' => [Functions::class, 'DUMMY'],
390
            'argumentCount' => '2',
391
        ],
392
        'CHOOSE' => [
393
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
394
            'functionCall' => [LookupRef::class, 'CHOOSE'],
395
            'argumentCount' => '2+',
396
        ],
397
        'CLEAN' => [
398
            'category' => Category::CATEGORY_TEXT_AND_DATA,
399
            'functionCall' => [TextData::class, 'TRIMNONPRINTABLE'],
400
            'argumentCount' => '1',
401
        ],
402
        'CODE' => [
403
            'category' => Category::CATEGORY_TEXT_AND_DATA,
404
            'functionCall' => [TextData::class, 'ASCIICODE'],
405
            'argumentCount' => '1',
406
        ],
407
        'COLUMN' => [
408
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
409
            'functionCall' => [LookupRef::class, 'COLUMN'],
410
            'argumentCount' => '-1',
411
            'passByReference' => [true],
412
        ],
413
        'COLUMNS' => [
414
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
415
            'functionCall' => [LookupRef::class, 'COLUMNS'],
416
            'argumentCount' => '1',
417
        ],
418
        'COMBIN' => [
419
            'category' => Category::CATEGORY_MATH_AND_TRIG,
420
            'functionCall' => [MathTrig::class, 'COMBIN'],
421
            'argumentCount' => '2',
422
        ],
423
        'COMPLEX' => [
424
            'category' => Category::CATEGORY_ENGINEERING,
425
            'functionCall' => [Engineering::class, 'COMPLEX'],
426
            'argumentCount' => '2,3',
427
        ],
428
        'CONCATENATE' => [
429
            'category' => Category::CATEGORY_TEXT_AND_DATA,
430
            'functionCall' => [TextData::class, 'CONCATENATE'],
431
            'argumentCount' => '1+',
432
        ],
433
        'CONFIDENCE' => [
434
            'category' => Category::CATEGORY_STATISTICAL,
435
            'functionCall' => [Statistical::class, 'CONFIDENCE'],
436
            'argumentCount' => '3',
437
        ],
438
        'CONVERT' => [
439
            'category' => Category::CATEGORY_ENGINEERING,
440
            'functionCall' => [Engineering::class, 'CONVERTUOM'],
441
            'argumentCount' => '3',
442
        ],
443
        'CORREL' => [
444
            'category' => Category::CATEGORY_STATISTICAL,
445
            'functionCall' => [Statistical::class, 'CORREL'],
446
            'argumentCount' => '2',
447
        ],
448
        'COS' => [
449
            'category' => Category::CATEGORY_MATH_AND_TRIG,
450
            'functionCall' => 'cos',
451
            'argumentCount' => '1',
452
        ],
453
        'COSH' => [
454
            'category' => Category::CATEGORY_MATH_AND_TRIG,
455
            'functionCall' => 'cosh',
456
            'argumentCount' => '1',
457
        ],
458
        'COUNT' => [
459
            'category' => Category::CATEGORY_STATISTICAL,
460
            'functionCall' => [Statistical::class, 'COUNT'],
461
            'argumentCount' => '1+',
462
        ],
463
        'COUNTA' => [
464
            'category' => Category::CATEGORY_STATISTICAL,
465
            'functionCall' => [Statistical::class, 'COUNTA'],
466
            'argumentCount' => '1+',
467
        ],
468
        'COUNTBLANK' => [
469
            'category' => Category::CATEGORY_STATISTICAL,
470
            'functionCall' => [Statistical::class, 'COUNTBLANK'],
471
            'argumentCount' => '1',
472
        ],
473
        'COUNTIF' => [
474
            'category' => Category::CATEGORY_STATISTICAL,
475
            'functionCall' => [Statistical::class, 'COUNTIF'],
476
            'argumentCount' => '2',
477
        ],
478
        'COUNTIFS' => [
479
            'category' => Category::CATEGORY_STATISTICAL,
480
            'functionCall' => [Functions::class, 'DUMMY'],
481
            'argumentCount' => '2',
482
        ],
483
        'COUPDAYBS' => [
484
            'category' => Category::CATEGORY_FINANCIAL,
485
            'functionCall' => [Financial::class, 'COUPDAYBS'],
486
            'argumentCount' => '3,4',
487
        ],
488
        'COUPDAYS' => [
489
            'category' => Category::CATEGORY_FINANCIAL,
490
            'functionCall' => [Financial::class, 'COUPDAYS'],
491
            'argumentCount' => '3,4',
492
        ],
493
        'COUPDAYSNC' => [
494
            'category' => Category::CATEGORY_FINANCIAL,
495
            'functionCall' => [Financial::class, 'COUPDAYSNC'],
496
            'argumentCount' => '3,4',
497
        ],
498
        'COUPNCD' => [
499
            'category' => Category::CATEGORY_FINANCIAL,
500
            'functionCall' => [Financial::class, 'COUPNCD'],
501
            'argumentCount' => '3,4',
502
        ],
503
        'COUPNUM' => [
504
            'category' => Category::CATEGORY_FINANCIAL,
505
            'functionCall' => [Financial::class, 'COUPNUM'],
506
            'argumentCount' => '3,4',
507
        ],
508
        'COUPPCD' => [
509
            'category' => Category::CATEGORY_FINANCIAL,
510
            'functionCall' => [Financial::class, 'COUPPCD'],
511
            'argumentCount' => '3,4',
512
        ],
513
        'COVAR' => [
514
            'category' => Category::CATEGORY_STATISTICAL,
515
            'functionCall' => [Statistical::class, 'COVAR'],
516
            'argumentCount' => '2',
517
        ],
518
        'CRITBINOM' => [
519
            'category' => Category::CATEGORY_STATISTICAL,
520
            'functionCall' => [Statistical::class, 'CRITBINOM'],
521
            'argumentCount' => '3',
522
        ],
523
        'CUBEKPIMEMBER' => [
524
            'category' => Category::CATEGORY_CUBE,
525
            'functionCall' => [Functions::class, 'DUMMY'],
526
            'argumentCount' => '?',
527
        ],
528
        'CUBEMEMBER' => [
529
            'category' => Category::CATEGORY_CUBE,
530
            'functionCall' => [Functions::class, 'DUMMY'],
531
            'argumentCount' => '?',
532
        ],
533
        'CUBEMEMBERPROPERTY' => [
534
            'category' => Category::CATEGORY_CUBE,
535
            'functionCall' => [Functions::class, 'DUMMY'],
536
            'argumentCount' => '?',
537
        ],
538
        'CUBERANKEDMEMBER' => [
539
            'category' => Category::CATEGORY_CUBE,
540
            'functionCall' => [Functions::class, 'DUMMY'],
541
            'argumentCount' => '?',
542
        ],
543
        'CUBESET' => [
544
            'category' => Category::CATEGORY_CUBE,
545
            'functionCall' => [Functions::class, 'DUMMY'],
546
            'argumentCount' => '?',
547
        ],
548
        'CUBESETCOUNT' => [
549
            'category' => Category::CATEGORY_CUBE,
550
            'functionCall' => [Functions::class, 'DUMMY'],
551
            'argumentCount' => '?',
552
        ],
553
        'CUBEVALUE' => [
554
            'category' => Category::CATEGORY_CUBE,
555
            'functionCall' => [Functions::class, 'DUMMY'],
556
            'argumentCount' => '?',
557
        ],
558
        'CUMIPMT' => [
559
            'category' => Category::CATEGORY_FINANCIAL,
560
            'functionCall' => [Financial::class, 'CUMIPMT'],
561
            'argumentCount' => '6',
562
        ],
563
        'CUMPRINC' => [
564
            'category' => Category::CATEGORY_FINANCIAL,
565
            'functionCall' => [Financial::class, 'CUMPRINC'],
566
            'argumentCount' => '6',
567
        ],
568
        'DATE' => [
569
            'category' => Category::CATEGORY_DATE_AND_TIME,
570
            'functionCall' => [DateTime::class, 'DATE'],
571
            'argumentCount' => '3',
572
        ],
573
        'DATEDIF' => [
574
            'category' => Category::CATEGORY_DATE_AND_TIME,
575
            'functionCall' => [DateTime::class, 'DATEDIF'],
576
            'argumentCount' => '2,3',
577
        ],
578
        'DATEVALUE' => [
579
            'category' => Category::CATEGORY_DATE_AND_TIME,
580
            'functionCall' => [DateTime::class, 'DATEVALUE'],
581
            'argumentCount' => '1',
582
        ],
583
        'DAVERAGE' => [
584
            'category' => Category::CATEGORY_DATABASE,
585
            'functionCall' => [Database::class, 'DAVERAGE'],
586
            'argumentCount' => '3',
587
        ],
588
        'DAY' => [
589
            'category' => Category::CATEGORY_DATE_AND_TIME,
590
            'functionCall' => [DateTime::class, 'DAYOFMONTH'],
591
            'argumentCount' => '1',
592
        ],
593
        'DAYS360' => [
594
            'category' => Category::CATEGORY_DATE_AND_TIME,
595
            'functionCall' => [DateTime::class, 'DAYS360'],
596
            'argumentCount' => '2,3',
597
        ],
598
        'DB' => [
599
            'category' => Category::CATEGORY_FINANCIAL,
600
            'functionCall' => [Financial::class, 'DB'],
601
            'argumentCount' => '4,5',
602
        ],
603
        'DCOUNT' => [
604
            'category' => Category::CATEGORY_DATABASE,
605
            'functionCall' => [Database::class, 'DCOUNT'],
606
            'argumentCount' => '3',
607
        ],
608
        'DCOUNTA' => [
609
            'category' => Category::CATEGORY_DATABASE,
610
            'functionCall' => [Database::class, 'DCOUNTA'],
611
            'argumentCount' => '3',
612
        ],
613
        'DDB' => [
614
            'category' => Category::CATEGORY_FINANCIAL,
615
            'functionCall' => [Financial::class, 'DDB'],
616
            'argumentCount' => '4,5',
617
        ],
618
        'DEC2BIN' => [
619
            'category' => Category::CATEGORY_ENGINEERING,
620
            'functionCall' => [Engineering::class, 'DECTOBIN'],
621
            'argumentCount' => '1,2',
622
        ],
623
        'DEC2HEX' => [
624
            'category' => Category::CATEGORY_ENGINEERING,
625
            'functionCall' => [Engineering::class, 'DECTOHEX'],
626
            'argumentCount' => '1,2',
627
        ],
628
        'DEC2OCT' => [
629
            'category' => Category::CATEGORY_ENGINEERING,
630
            'functionCall' => [Engineering::class, 'DECTOOCT'],
631
            'argumentCount' => '1,2',
632
        ],
633
        'DEGREES' => [
634
            'category' => Category::CATEGORY_MATH_AND_TRIG,
635
            'functionCall' => 'rad2deg',
636
            'argumentCount' => '1',
637
        ],
638
        'DELTA' => [
639
            'category' => Category::CATEGORY_ENGINEERING,
640
            'functionCall' => [Engineering::class, 'DELTA'],
641
            'argumentCount' => '1,2',
642
        ],
643
        'DEVSQ' => [
644
            'category' => Category::CATEGORY_STATISTICAL,
645
            'functionCall' => [Statistical::class, 'DEVSQ'],
646
            'argumentCount' => '1+',
647
        ],
648
        'DGET' => [
649
            'category' => Category::CATEGORY_DATABASE,
650
            'functionCall' => [Database::class, 'DGET'],
651
            'argumentCount' => '3',
652
        ],
653
        'DISC' => [
654
            'category' => Category::CATEGORY_FINANCIAL,
655
            'functionCall' => [Financial::class, 'DISC'],
656
            'argumentCount' => '4,5',
657
        ],
658
        'DMAX' => [
659
            'category' => Category::CATEGORY_DATABASE,
660
            'functionCall' => [Database::class, 'DMAX'],
661
            'argumentCount' => '3',
662
        ],
663
        'DMIN' => [
664
            'category' => Category::CATEGORY_DATABASE,
665
            'functionCall' => [Database::class, 'DMIN'],
666
            'argumentCount' => '3',
667
        ],
668
        'DOLLAR' => [
669
            'category' => Category::CATEGORY_TEXT_AND_DATA,
670
            'functionCall' => [TextData::class, 'DOLLAR'],
671
            'argumentCount' => '1,2',
672
        ],
673
        'DOLLARDE' => [
674
            'category' => Category::CATEGORY_FINANCIAL,
675
            'functionCall' => [Financial::class, 'DOLLARDE'],
676
            'argumentCount' => '2',
677
        ],
678
        'DOLLARFR' => [
679
            'category' => Category::CATEGORY_FINANCIAL,
680
            'functionCall' => [Financial::class, 'DOLLARFR'],
681
            'argumentCount' => '2',
682
        ],
683
        'DPRODUCT' => [
684
            'category' => Category::CATEGORY_DATABASE,
685
            'functionCall' => [Database::class, 'DPRODUCT'],
686
            'argumentCount' => '3',
687
        ],
688
        'DSTDEV' => [
689
            'category' => Category::CATEGORY_DATABASE,
690
            'functionCall' => [Database::class, 'DSTDEV'],
691
            'argumentCount' => '3',
692
        ],
693
        'DSTDEVP' => [
694
            'category' => Category::CATEGORY_DATABASE,
695
            'functionCall' => [Database::class, 'DSTDEVP'],
696
            'argumentCount' => '3',
697
        ],
698
        'DSUM' => [
699
            'category' => Category::CATEGORY_DATABASE,
700
            'functionCall' => [Database::class, 'DSUM'],
701
            'argumentCount' => '3',
702
        ],
703
        'DURATION' => [
704
            'category' => Category::CATEGORY_FINANCIAL,
705
            'functionCall' => [Functions::class, 'DUMMY'],
706
            'argumentCount' => '5,6',
707
        ],
708
        'DVAR' => [
709
            'category' => Category::CATEGORY_DATABASE,
710
            'functionCall' => [Database::class, 'DVAR'],
711
            'argumentCount' => '3',
712
        ],
713
        'DVARP' => [
714
            'category' => Category::CATEGORY_DATABASE,
715
            'functionCall' => [Database::class, 'DVARP'],
716
            'argumentCount' => '3',
717
        ],
718
        'EDATE' => [
719
            'category' => Category::CATEGORY_DATE_AND_TIME,
720
            'functionCall' => [DateTime::class, 'EDATE'],
721
            'argumentCount' => '2',
722
        ],
723
        'EFFECT' => [
724
            'category' => Category::CATEGORY_FINANCIAL,
725
            'functionCall' => [Financial::class, 'EFFECT'],
726
            'argumentCount' => '2',
727
        ],
728
        'EOMONTH' => [
729
            'category' => Category::CATEGORY_DATE_AND_TIME,
730
            'functionCall' => [DateTime::class, 'EOMONTH'],
731
            'argumentCount' => '2',
732
        ],
733
        'ERF' => [
734
            'category' => Category::CATEGORY_ENGINEERING,
735
            'functionCall' => [Engineering::class, 'ERF'],
736
            'argumentCount' => '1,2',
737
        ],
738
        'ERFC' => [
739
            'category' => Category::CATEGORY_ENGINEERING,
740
            'functionCall' => [Engineering::class, 'ERFC'],
741
            'argumentCount' => '1',
742
        ],
743
        'ERROR.TYPE' => [
744
            'category' => Category::CATEGORY_INFORMATION,
745
            'functionCall' => [Functions::class, 'errorType'],
746
            'argumentCount' => '1',
747
        ],
748
        'EVEN' => [
749
            'category' => Category::CATEGORY_MATH_AND_TRIG,
750
            'functionCall' => [MathTrig::class, 'EVEN'],
751
            'argumentCount' => '1',
752
        ],
753
        'EXACT' => [
754
            'category' => Category::CATEGORY_TEXT_AND_DATA,
755
            'functionCall' => [Functions::class, 'DUMMY'],
756
            'argumentCount' => '2',
757
        ],
758
        'EXP' => [
759
            'category' => Category::CATEGORY_MATH_AND_TRIG,
760
            'functionCall' => 'exp',
761
            'argumentCount' => '1',
762
        ],
763
        'EXPONDIST' => [
764
            'category' => Category::CATEGORY_STATISTICAL,
765
            'functionCall' => [Statistical::class, 'EXPONDIST'],
766
            'argumentCount' => '3',
767
        ],
768
        'FACT' => [
769
            'category' => Category::CATEGORY_MATH_AND_TRIG,
770
            'functionCall' => [MathTrig::class, 'FACT'],
771
            'argumentCount' => '1',
772
        ],
773
        'FACTDOUBLE' => [
774
            'category' => Category::CATEGORY_MATH_AND_TRIG,
775
            'functionCall' => [MathTrig::class, 'FACTDOUBLE'],
776
            'argumentCount' => '1',
777
        ],
778
        'FALSE' => [
779
            'category' => Category::CATEGORY_LOGICAL,
780
            'functionCall' => [Logical::class, 'FALSE'],
781
            'argumentCount' => '0',
782
        ],
783
        'FDIST' => [
784
            'category' => Category::CATEGORY_STATISTICAL,
785
            'functionCall' => [Functions::class, 'DUMMY'],
786
            'argumentCount' => '3',
787
        ],
788
        'FIND' => [
789
            'category' => Category::CATEGORY_TEXT_AND_DATA,
790
            'functionCall' => [TextData::class, 'SEARCHSENSITIVE'],
791
            'argumentCount' => '2,3',
792
        ],
793
        'FINDB' => [
794
            'category' => Category::CATEGORY_TEXT_AND_DATA,
795
            'functionCall' => [TextData::class, 'SEARCHSENSITIVE'],
796
            'argumentCount' => '2,3',
797
        ],
798
        'FINV' => [
799
            'category' => Category::CATEGORY_STATISTICAL,
800
            'functionCall' => [Functions::class, 'DUMMY'],
801
            'argumentCount' => '3',
802
        ],
803
        'FISHER' => [
804
            'category' => Category::CATEGORY_STATISTICAL,
805
            'functionCall' => [Statistical::class, 'FISHER'],
806
            'argumentCount' => '1',
807
        ],
808
        'FISHERINV' => [
809
            'category' => Category::CATEGORY_STATISTICAL,
810
            'functionCall' => [Statistical::class, 'FISHERINV'],
811
            'argumentCount' => '1',
812
        ],
813
        'FIXED' => [
814
            'category' => Category::CATEGORY_TEXT_AND_DATA,
815
            'functionCall' => [TextData::class, 'FIXEDFORMAT'],
816
            'argumentCount' => '1-3',
817
        ],
818
        'FLOOR' => [
819
            'category' => Category::CATEGORY_MATH_AND_TRIG,
820
            'functionCall' => [MathTrig::class, 'FLOOR'],
821
            'argumentCount' => '2',
822
        ],
823
        'FORECAST' => [
824
            'category' => Category::CATEGORY_STATISTICAL,
825
            'functionCall' => [Statistical::class, 'FORECAST'],
826
            'argumentCount' => '3',
827
        ],
828
        'FREQUENCY' => [
829
            'category' => Category::CATEGORY_STATISTICAL,
830
            'functionCall' => [Functions::class, 'DUMMY'],
831
            'argumentCount' => '2',
832
        ],
833
        'FTEST' => [
834
            'category' => Category::CATEGORY_STATISTICAL,
835
            'functionCall' => [Functions::class, 'DUMMY'],
836
            'argumentCount' => '2',
837
        ],
838
        'FV' => [
839
            'category' => Category::CATEGORY_FINANCIAL,
840
            'functionCall' => [Financial::class, 'FV'],
841
            'argumentCount' => '3-5',
842
        ],
843
        'FVSCHEDULE' => [
844
            'category' => Category::CATEGORY_FINANCIAL,
845
            'functionCall' => [Financial::class, 'FVSCHEDULE'],
846
            'argumentCount' => '2',
847
        ],
848
        'GAMMADIST' => [
849
            'category' => Category::CATEGORY_STATISTICAL,
850
            'functionCall' => [Statistical::class, 'GAMMADIST'],
851
            'argumentCount' => '4',
852
        ],
853
        'GAMMAINV' => [
854
            'category' => Category::CATEGORY_STATISTICAL,
855
            'functionCall' => [Statistical::class, 'GAMMAINV'],
856
            'argumentCount' => '3',
857
        ],
858
        'GAMMALN' => [
859
            'category' => Category::CATEGORY_STATISTICAL,
860
            'functionCall' => [Statistical::class, 'GAMMALN'],
861
            'argumentCount' => '1',
862
        ],
863
        'GCD' => [
864
            'category' => Category::CATEGORY_MATH_AND_TRIG,
865
            'functionCall' => [MathTrig::class, 'GCD'],
866
            'argumentCount' => '1+',
867
        ],
868
        'GEOMEAN' => [
869
            'category' => Category::CATEGORY_STATISTICAL,
870
            'functionCall' => [Statistical::class, 'GEOMEAN'],
871
            'argumentCount' => '1+',
872
        ],
873
        'GESTEP' => [
874
            'category' => Category::CATEGORY_ENGINEERING,
875
            'functionCall' => [Engineering::class, 'GESTEP'],
876
            'argumentCount' => '1,2',
877
        ],
878
        'GETPIVOTDATA' => [
879
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
880
            'functionCall' => [Functions::class, 'DUMMY'],
881
            'argumentCount' => '2+',
882
        ],
883
        'GROWTH' => [
884
            'category' => Category::CATEGORY_STATISTICAL,
885
            'functionCall' => [Statistical::class, 'GROWTH'],
886
            'argumentCount' => '1-4',
887
        ],
888
        'HARMEAN' => [
889
            'category' => Category::CATEGORY_STATISTICAL,
890
            'functionCall' => [Statistical::class, 'HARMEAN'],
891
            'argumentCount' => '1+',
892
        ],
893
        'HEX2BIN' => [
894
            'category' => Category::CATEGORY_ENGINEERING,
895
            'functionCall' => [Engineering::class, 'HEXTOBIN'],
896
            'argumentCount' => '1,2',
897
        ],
898
        'HEX2DEC' => [
899
            'category' => Category::CATEGORY_ENGINEERING,
900
            'functionCall' => [Engineering::class, 'HEXTODEC'],
901
            'argumentCount' => '1',
902
        ],
903
        'HEX2OCT' => [
904
            'category' => Category::CATEGORY_ENGINEERING,
905
            'functionCall' => [Engineering::class, 'HEXTOOCT'],
906
            'argumentCount' => '1,2',
907
        ],
908
        'HLOOKUP' => [
909
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
910
            'functionCall' => [LookupRef::class, 'HLOOKUP'],
911
            'argumentCount' => '3,4',
912
        ],
913
        'HOUR' => [
914
            'category' => Category::CATEGORY_DATE_AND_TIME,
915
            'functionCall' => [DateTime::class, 'HOUROFDAY'],
916
            'argumentCount' => '1',
917
        ],
918
        'HYPERLINK' => [
919
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
920
            'functionCall' => [LookupRef::class, 'HYPERLINK'],
921
            'argumentCount' => '1,2',
922
            'passCellReference' => true,
923
        ],
924
        'HYPGEOMDIST' => [
925
            'category' => Category::CATEGORY_STATISTICAL,
926
            'functionCall' => [Statistical::class, 'HYPGEOMDIST'],
927
            'argumentCount' => '4',
928
        ],
929
        'IF' => [
930
            'category' => Category::CATEGORY_LOGICAL,
931
            'functionCall' => [Logical::class, 'statementIf'],
932
            'argumentCount' => '1-3',
933
        ],
934
        'IFERROR' => [
935
            'category' => Category::CATEGORY_LOGICAL,
936
            'functionCall' => [Logical::class, 'IFERROR'],
937
            'argumentCount' => '2',
938
        ],
939
        'IMABS' => [
940
            'category' => Category::CATEGORY_ENGINEERING,
941
            'functionCall' => [Engineering::class, 'IMABS'],
942
            'argumentCount' => '1',
943
        ],
944
        'IMAGINARY' => [
945
            'category' => Category::CATEGORY_ENGINEERING,
946
            'functionCall' => [Engineering::class, 'IMAGINARY'],
947
            'argumentCount' => '1',
948
        ],
949
        'IMARGUMENT' => [
950
            'category' => Category::CATEGORY_ENGINEERING,
951
            'functionCall' => [Engineering::class, 'IMARGUMENT'],
952
            'argumentCount' => '1',
953
        ],
954
        'IMCONJUGATE' => [
955
            'category' => Category::CATEGORY_ENGINEERING,
956
            'functionCall' => [Engineering::class, 'IMCONJUGATE'],
957
            'argumentCount' => '1',
958
        ],
959
        'IMCOS' => [
960
            'category' => Category::CATEGORY_ENGINEERING,
961
            'functionCall' => [Engineering::class, 'IMCOS'],
962
            'argumentCount' => '1',
963
        ],
964
        'IMDIV' => [
965
            'category' => Category::CATEGORY_ENGINEERING,
966
            'functionCall' => [Engineering::class, 'IMDIV'],
967
            'argumentCount' => '2',
968
        ],
969
        'IMEXP' => [
970
            'category' => Category::CATEGORY_ENGINEERING,
971
            'functionCall' => [Engineering::class, 'IMEXP'],
972
            'argumentCount' => '1',
973
        ],
974
        'IMLN' => [
975
            'category' => Category::CATEGORY_ENGINEERING,
976
            'functionCall' => [Engineering::class, 'IMLN'],
977
            'argumentCount' => '1',
978
        ],
979
        'IMLOG10' => [
980
            'category' => Category::CATEGORY_ENGINEERING,
981
            'functionCall' => [Engineering::class, 'IMLOG10'],
982
            'argumentCount' => '1',
983
        ],
984
        'IMLOG2' => [
985
            'category' => Category::CATEGORY_ENGINEERING,
986
            'functionCall' => [Engineering::class, 'IMLOG2'],
987
            'argumentCount' => '1',
988
        ],
989
        'IMPOWER' => [
990
            'category' => Category::CATEGORY_ENGINEERING,
991
            'functionCall' => [Engineering::class, 'IMPOWER'],
992
            'argumentCount' => '2',
993
        ],
994
        'IMPRODUCT' => [
995
            'category' => Category::CATEGORY_ENGINEERING,
996
            'functionCall' => [Engineering::class, 'IMPRODUCT'],
997
            'argumentCount' => '1+',
998
        ],
999
        'IMREAL' => [
1000
            'category' => Category::CATEGORY_ENGINEERING,
1001
            'functionCall' => [Engineering::class, 'IMREAL'],
1002
            'argumentCount' => '1',
1003
        ],
1004
        'IMSIN' => [
1005
            'category' => Category::CATEGORY_ENGINEERING,
1006
            'functionCall' => [Engineering::class, 'IMSIN'],
1007
            'argumentCount' => '1',
1008
        ],
1009
        'IMSQRT' => [
1010
            'category' => Category::CATEGORY_ENGINEERING,
1011
            'functionCall' => [Engineering::class, 'IMSQRT'],
1012
            'argumentCount' => '1',
1013
        ],
1014
        'IMSUB' => [
1015
            'category' => Category::CATEGORY_ENGINEERING,
1016
            'functionCall' => [Engineering::class, 'IMSUB'],
1017
            'argumentCount' => '2',
1018
        ],
1019
        'IMSUM' => [
1020
            'category' => Category::CATEGORY_ENGINEERING,
1021
            'functionCall' => [Engineering::class, 'IMSUM'],
1022
            'argumentCount' => '1+',
1023
        ],
1024
        'INDEX' => [
1025
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1026
            'functionCall' => [LookupRef::class, 'INDEX'],
1027
            'argumentCount' => '1-4',
1028
        ],
1029
        'INDIRECT' => [
1030
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1031
            'functionCall' => [LookupRef::class, 'INDIRECT'],
1032
            'argumentCount' => '1,2',
1033
            'passCellReference' => true,
1034
        ],
1035
        'INFO' => [
1036
            'category' => Category::CATEGORY_INFORMATION,
1037
            'functionCall' => [Functions::class, 'DUMMY'],
1038
            'argumentCount' => '1',
1039
        ],
1040
        'INT' => [
1041
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1042
            'functionCall' => [MathTrig::class, 'INT'],
1043
            'argumentCount' => '1',
1044
        ],
1045
        'INTERCEPT' => [
1046
            'category' => Category::CATEGORY_STATISTICAL,
1047
            'functionCall' => [Statistical::class, 'INTERCEPT'],
1048
            'argumentCount' => '2',
1049
        ],
1050
        'INTRATE' => [
1051
            'category' => Category::CATEGORY_FINANCIAL,
1052
            'functionCall' => [Financial::class, 'INTRATE'],
1053
            'argumentCount' => '4,5',
1054
        ],
1055
        'IPMT' => [
1056
            'category' => Category::CATEGORY_FINANCIAL,
1057
            'functionCall' => [Financial::class, 'IPMT'],
1058
            'argumentCount' => '4-6',
1059
        ],
1060
        'IRR' => [
1061
            'category' => Category::CATEGORY_FINANCIAL,
1062
            'functionCall' => [Financial::class, 'IRR'],
1063
            'argumentCount' => '1,2',
1064
        ],
1065
        'ISBLANK' => [
1066
            'category' => Category::CATEGORY_INFORMATION,
1067
            'functionCall' => [Functions::class, 'isBlank'],
1068
            'argumentCount' => '1',
1069
        ],
1070
        'ISERR' => [
1071
            'category' => Category::CATEGORY_INFORMATION,
1072
            'functionCall' => [Functions::class, 'isErr'],
1073
            'argumentCount' => '1',
1074
        ],
1075
        'ISERROR' => [
1076
            'category' => Category::CATEGORY_INFORMATION,
1077
            'functionCall' => [Functions::class, 'isError'],
1078
            'argumentCount' => '1',
1079
        ],
1080
        'ISEVEN' => [
1081
            'category' => Category::CATEGORY_INFORMATION,
1082
            'functionCall' => [Functions::class, 'isEven'],
1083
            'argumentCount' => '1',
1084
        ],
1085
        'ISLOGICAL' => [
1086
            'category' => Category::CATEGORY_INFORMATION,
1087
            'functionCall' => [Functions::class, 'isLogical'],
1088
            'argumentCount' => '1',
1089
        ],
1090
        'ISNA' => [
1091
            'category' => Category::CATEGORY_INFORMATION,
1092
            'functionCall' => [Functions::class, 'isNa'],
1093
            'argumentCount' => '1',
1094
        ],
1095
        'ISNONTEXT' => [
1096
            'category' => Category::CATEGORY_INFORMATION,
1097
            'functionCall' => [Functions::class, 'isNonText'],
1098
            'argumentCount' => '1',
1099
        ],
1100
        'ISNUMBER' => [
1101
            'category' => Category::CATEGORY_INFORMATION,
1102
            'functionCall' => [Functions::class, 'isNumber'],
1103
            'argumentCount' => '1',
1104
        ],
1105
        'ISODD' => [
1106
            'category' => Category::CATEGORY_INFORMATION,
1107
            'functionCall' => [Functions::class, 'isOdd'],
1108
            'argumentCount' => '1',
1109
        ],
1110
        'ISPMT' => [
1111
            'category' => Category::CATEGORY_FINANCIAL,
1112
            'functionCall' => [Financial::class, 'ISPMT'],
1113
            'argumentCount' => '4',
1114
        ],
1115
        'ISREF' => [
1116
            'category' => Category::CATEGORY_INFORMATION,
1117
            'functionCall' => [Functions::class, 'DUMMY'],
1118
            'argumentCount' => '1',
1119
        ],
1120
        'ISTEXT' => [
1121
            'category' => Category::CATEGORY_INFORMATION,
1122
            'functionCall' => [Functions::class, 'isText'],
1123
            'argumentCount' => '1',
1124
        ],
1125
        'JIS' => [
1126
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1127
            'functionCall' => [Functions::class, 'DUMMY'],
1128
            'argumentCount' => '1',
1129
        ],
1130
        'KURT' => [
1131
            'category' => Category::CATEGORY_STATISTICAL,
1132
            'functionCall' => [Statistical::class, 'KURT'],
1133
            'argumentCount' => '1+',
1134
        ],
1135
        'LARGE' => [
1136
            'category' => Category::CATEGORY_STATISTICAL,
1137
            'functionCall' => [Statistical::class, 'LARGE'],
1138
            'argumentCount' => '2',
1139
        ],
1140
        'LCM' => [
1141
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1142
            'functionCall' => [MathTrig::class, 'LCM'],
1143
            'argumentCount' => '1+',
1144
        ],
1145
        'LEFT' => [
1146
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1147
            'functionCall' => [TextData::class, 'LEFT'],
1148
            'argumentCount' => '1,2',
1149
        ],
1150
        'LEFTB' => [
1151
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1152
            'functionCall' => [TextData::class, 'LEFT'],
1153
            'argumentCount' => '1,2',
1154
        ],
1155
        'LEN' => [
1156
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1157
            'functionCall' => [TextData::class, 'STRINGLENGTH'],
1158
            'argumentCount' => '1',
1159
        ],
1160
        'LENB' => [
1161
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1162
            'functionCall' => [TextData::class, 'STRINGLENGTH'],
1163
            'argumentCount' => '1',
1164
        ],
1165
        'LINEST' => [
1166
            'category' => Category::CATEGORY_STATISTICAL,
1167
            'functionCall' => [Statistical::class, 'LINEST'],
1168
            'argumentCount' => '1-4',
1169
        ],
1170
        'LN' => [
1171
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1172
            'functionCall' => 'log',
1173
            'argumentCount' => '1',
1174
        ],
1175
        'LOG' => [
1176
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1177
            'functionCall' => [MathTrig::class, 'logBase'],
1178
            'argumentCount' => '1,2',
1179
        ],
1180
        'LOG10' => [
1181
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1182
            'functionCall' => 'log10',
1183
            'argumentCount' => '1',
1184
        ],
1185
        'LOGEST' => [
1186
            'category' => Category::CATEGORY_STATISTICAL,
1187
            'functionCall' => [Statistical::class, 'LOGEST'],
1188
            'argumentCount' => '1-4',
1189
        ],
1190
        'LOGINV' => [
1191
            'category' => Category::CATEGORY_STATISTICAL,
1192
            'functionCall' => [Statistical::class, 'LOGINV'],
1193
            'argumentCount' => '3',
1194
        ],
1195
        'LOGNORMDIST' => [
1196
            'category' => Category::CATEGORY_STATISTICAL,
1197
            'functionCall' => [Statistical::class, 'LOGNORMDIST'],
1198
            'argumentCount' => '3',
1199
        ],
1200
        'LOOKUP' => [
1201
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1202
            'functionCall' => [LookupRef::class, 'LOOKUP'],
1203
            'argumentCount' => '2,3',
1204
        ],
1205
        'LOWER' => [
1206
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1207
            'functionCall' => [TextData::class, 'LOWERCASE'],
1208
            'argumentCount' => '1',
1209
        ],
1210
        'MATCH' => [
1211
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1212
            'functionCall' => [LookupRef::class, 'MATCH'],
1213
            'argumentCount' => '2,3',
1214
        ],
1215
        'MAX' => [
1216
            'category' => Category::CATEGORY_STATISTICAL,
1217
            'functionCall' => [Statistical::class, 'MAX'],
1218
            'argumentCount' => '1+',
1219
        ],
1220
        'MAXA' => [
1221
            'category' => Category::CATEGORY_STATISTICAL,
1222
            'functionCall' => [Statistical::class, 'MAXA'],
1223
            'argumentCount' => '1+',
1224
        ],
1225
        'MAXIF' => [
1226
            'category' => Category::CATEGORY_STATISTICAL,
1227
            'functionCall' => [Statistical::class, 'MAXIF'],
1228
            'argumentCount' => '2+',
1229
        ],
1230
        'MDETERM' => [
1231
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1232
            'functionCall' => [MathTrig::class, 'MDETERM'],
1233
            'argumentCount' => '1',
1234
        ],
1235
        'MDURATION' => [
1236
            'category' => Category::CATEGORY_FINANCIAL,
1237
            'functionCall' => [Functions::class, 'DUMMY'],
1238
            'argumentCount' => '5,6',
1239
        ],
1240
        'MEDIAN' => [
1241
            'category' => Category::CATEGORY_STATISTICAL,
1242
            'functionCall' => [Statistical::class, 'MEDIAN'],
1243
            'argumentCount' => '1+',
1244
        ],
1245
        'MEDIANIF' => [
1246
            'category' => Category::CATEGORY_STATISTICAL,
1247
            'functionCall' => [Functions::class, 'DUMMY'],
1248
            'argumentCount' => '2+',
1249
        ],
1250
        'MID' => [
1251
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1252
            'functionCall' => [TextData::class, 'MID'],
1253
            'argumentCount' => '3',
1254
        ],
1255
        'MIDB' => [
1256
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1257
            'functionCall' => [TextData::class, 'MID'],
1258
            'argumentCount' => '3',
1259
        ],
1260
        'MIN' => [
1261
            'category' => Category::CATEGORY_STATISTICAL,
1262
            'functionCall' => [Statistical::class, 'MIN'],
1263
            'argumentCount' => '1+',
1264
        ],
1265
        'MINA' => [
1266
            'category' => Category::CATEGORY_STATISTICAL,
1267
            'functionCall' => [Statistical::class, 'MINA'],
1268
            'argumentCount' => '1+',
1269
        ],
1270
        'MINIF' => [
1271
            'category' => Category::CATEGORY_STATISTICAL,
1272
            'functionCall' => [Statistical::class, 'MINIF'],
1273
            'argumentCount' => '2+',
1274
        ],
1275
        'MINUTE' => [
1276
            'category' => Category::CATEGORY_DATE_AND_TIME,
1277
            'functionCall' => [DateTime::class, 'MINUTE'],
1278
            'argumentCount' => '1',
1279
        ],
1280
        'MINVERSE' => [
1281
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1282
            'functionCall' => [MathTrig::class, 'MINVERSE'],
1283
            'argumentCount' => '1',
1284
        ],
1285
        'MIRR' => [
1286
            'category' => Category::CATEGORY_FINANCIAL,
1287
            'functionCall' => [Financial::class, 'MIRR'],
1288
            'argumentCount' => '3',
1289
        ],
1290
        'MMULT' => [
1291
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1292
            'functionCall' => [MathTrig::class, 'MMULT'],
1293
            'argumentCount' => '2',
1294
        ],
1295
        'MOD' => [
1296
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1297
            'functionCall' => [MathTrig::class, 'MOD'],
1298
            'argumentCount' => '2',
1299
        ],
1300
        'MODE' => [
1301
            'category' => Category::CATEGORY_STATISTICAL,
1302
            'functionCall' => [Statistical::class, 'MODE'],
1303
            'argumentCount' => '1+',
1304
        ],
1305
        'MONTH' => [
1306
            'category' => Category::CATEGORY_DATE_AND_TIME,
1307
            'functionCall' => [DateTime::class, 'MONTHOFYEAR'],
1308
            'argumentCount' => '1',
1309
        ],
1310
        'MROUND' => [
1311
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1312
            'functionCall' => [MathTrig::class, 'MROUND'],
1313
            'argumentCount' => '2',
1314
        ],
1315
        'MULTINOMIAL' => [
1316
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1317
            'functionCall' => [MathTrig::class, 'MULTINOMIAL'],
1318
            'argumentCount' => '1+',
1319
        ],
1320
        'N' => [
1321
            'category' => Category::CATEGORY_INFORMATION,
1322
            'functionCall' => [Functions::class, 'n'],
1323
            'argumentCount' => '1',
1324
        ],
1325
        'NA' => [
1326
            'category' => Category::CATEGORY_INFORMATION,
1327
            'functionCall' => [Functions::class, 'NA'],
1328
            'argumentCount' => '0',
1329
        ],
1330
        'NEGBINOMDIST' => [
1331
            'category' => Category::CATEGORY_STATISTICAL,
1332
            'functionCall' => [Statistical::class, 'NEGBINOMDIST'],
1333
            'argumentCount' => '3',
1334
        ],
1335
        'NETWORKDAYS' => [
1336
            'category' => Category::CATEGORY_DATE_AND_TIME,
1337
            'functionCall' => [DateTime::class, 'NETWORKDAYS'],
1338
            'argumentCount' => '2+',
1339
        ],
1340
        'NOMINAL' => [
1341
            'category' => Category::CATEGORY_FINANCIAL,
1342
            'functionCall' => [Financial::class, 'NOMINAL'],
1343
            'argumentCount' => '2',
1344
        ],
1345
        'NORMDIST' => [
1346
            'category' => Category::CATEGORY_STATISTICAL,
1347
            'functionCall' => [Statistical::class, 'NORMDIST'],
1348
            'argumentCount' => '4',
1349
        ],
1350
        'NORMINV' => [
1351
            'category' => Category::CATEGORY_STATISTICAL,
1352
            'functionCall' => [Statistical::class, 'NORMINV'],
1353
            'argumentCount' => '3',
1354
        ],
1355
        'NORMSDIST' => [
1356
            'category' => Category::CATEGORY_STATISTICAL,
1357
            'functionCall' => [Statistical::class, 'NORMSDIST'],
1358
            'argumentCount' => '1',
1359
        ],
1360
        'NORMSINV' => [
1361
            'category' => Category::CATEGORY_STATISTICAL,
1362
            'functionCall' => [Statistical::class, 'NORMSINV'],
1363
            'argumentCount' => '1',
1364
        ],
1365
        'NOT' => [
1366
            'category' => Category::CATEGORY_LOGICAL,
1367
            'functionCall' => [Logical::class, 'NOT'],
1368
            'argumentCount' => '1',
1369
        ],
1370
        'NOW' => [
1371
            'category' => Category::CATEGORY_DATE_AND_TIME,
1372
            'functionCall' => [DateTime::class, 'DATETIMENOW'],
1373
            'argumentCount' => '0',
1374
        ],
1375
        'NPER' => [
1376
            'category' => Category::CATEGORY_FINANCIAL,
1377
            'functionCall' => [Financial::class, 'NPER'],
1378
            'argumentCount' => '3-5',
1379
        ],
1380
        'NPV' => [
1381
            'category' => Category::CATEGORY_FINANCIAL,
1382
            'functionCall' => [Financial::class, 'NPV'],
1383
            'argumentCount' => '2+',
1384
        ],
1385
        'OCT2BIN' => [
1386
            'category' => Category::CATEGORY_ENGINEERING,
1387
            'functionCall' => [Engineering::class, 'OCTTOBIN'],
1388
            'argumentCount' => '1,2',
1389
        ],
1390
        'OCT2DEC' => [
1391
            'category' => Category::CATEGORY_ENGINEERING,
1392
            'functionCall' => [Engineering::class, 'OCTTODEC'],
1393
            'argumentCount' => '1',
1394
        ],
1395
        'OCT2HEX' => [
1396
            'category' => Category::CATEGORY_ENGINEERING,
1397
            'functionCall' => [Engineering::class, 'OCTTOHEX'],
1398
            'argumentCount' => '1,2',
1399
        ],
1400
        'ODD' => [
1401
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1402
            'functionCall' => [MathTrig::class, 'ODD'],
1403
            'argumentCount' => '1',
1404
        ],
1405
        'ODDFPRICE' => [
1406
            'category' => Category::CATEGORY_FINANCIAL,
1407
            'functionCall' => [Functions::class, 'DUMMY'],
1408
            'argumentCount' => '8,9',
1409
        ],
1410
        'ODDFYIELD' => [
1411
            'category' => Category::CATEGORY_FINANCIAL,
1412
            'functionCall' => [Functions::class, 'DUMMY'],
1413
            'argumentCount' => '8,9',
1414
        ],
1415
        'ODDLPRICE' => [
1416
            'category' => Category::CATEGORY_FINANCIAL,
1417
            'functionCall' => [Functions::class, 'DUMMY'],
1418
            'argumentCount' => '7,8',
1419
        ],
1420
        'ODDLYIELD' => [
1421
            'category' => Category::CATEGORY_FINANCIAL,
1422
            'functionCall' => [Functions::class, 'DUMMY'],
1423
            'argumentCount' => '7,8',
1424
        ],
1425
        'OFFSET' => [
1426
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1427
            'functionCall' => [LookupRef::class, 'OFFSET'],
1428
            'argumentCount' => '3-5',
1429
            'passCellReference' => true,
1430
            'passByReference' => [true],
1431
        ],
1432
        'OR' => [
1433
            'category' => Category::CATEGORY_LOGICAL,
1434
            'functionCall' => [Logical::class, 'logicalOr'],
1435
            'argumentCount' => '1+',
1436
        ],
1437
        'PEARSON' => [
1438
            'category' => Category::CATEGORY_STATISTICAL,
1439
            'functionCall' => [Statistical::class, 'CORREL'],
1440
            'argumentCount' => '2',
1441
        ],
1442
        'PERCENTILE' => [
1443
            'category' => Category::CATEGORY_STATISTICAL,
1444
            'functionCall' => [Statistical::class, 'PERCENTILE'],
1445
            'argumentCount' => '2',
1446
        ],
1447
        'PERCENTRANK' => [
1448
            'category' => Category::CATEGORY_STATISTICAL,
1449
            'functionCall' => [Statistical::class, 'PERCENTRANK'],
1450
            'argumentCount' => '2,3',
1451
        ],
1452
        'PERMUT' => [
1453
            'category' => Category::CATEGORY_STATISTICAL,
1454
            'functionCall' => [Statistical::class, 'PERMUT'],
1455
            'argumentCount' => '2',
1456
        ],
1457
        'PHONETIC' => [
1458
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1459
            'functionCall' => [Functions::class, 'DUMMY'],
1460
            'argumentCount' => '1',
1461
        ],
1462
        'PI' => [
1463
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1464
            'functionCall' => 'pi',
1465
            'argumentCount' => '0',
1466
        ],
1467
        'PMT' => [
1468
            'category' => Category::CATEGORY_FINANCIAL,
1469
            'functionCall' => [Financial::class, 'PMT'],
1470
            'argumentCount' => '3-5',
1471
        ],
1472
        'POISSON' => [
1473
            'category' => Category::CATEGORY_STATISTICAL,
1474
            'functionCall' => [Statistical::class, 'POISSON'],
1475
            'argumentCount' => '3',
1476
        ],
1477
        'POWER' => [
1478
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1479
            'functionCall' => [MathTrig::class, 'POWER'],
1480
            'argumentCount' => '2',
1481
        ],
1482
        'PPMT' => [
1483
            'category' => Category::CATEGORY_FINANCIAL,
1484
            'functionCall' => [Financial::class, 'PPMT'],
1485
            'argumentCount' => '4-6',
1486
        ],
1487
        'PRICE' => [
1488
            'category' => Category::CATEGORY_FINANCIAL,
1489
            'functionCall' => [Financial::class, 'PRICE'],
1490
            'argumentCount' => '6,7',
1491
        ],
1492
        'PRICEDISC' => [
1493
            'category' => Category::CATEGORY_FINANCIAL,
1494
            'functionCall' => [Financial::class, 'PRICEDISC'],
1495
            'argumentCount' => '4,5',
1496
        ],
1497
        'PRICEMAT' => [
1498
            'category' => Category::CATEGORY_FINANCIAL,
1499
            'functionCall' => [Financial::class, 'PRICEMAT'],
1500
            'argumentCount' => '5,6',
1501
        ],
1502
        'PROB' => [
1503
            'category' => Category::CATEGORY_STATISTICAL,
1504
            'functionCall' => [Functions::class, 'DUMMY'],
1505
            'argumentCount' => '3,4',
1506
        ],
1507
        'PRODUCT' => [
1508
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1509
            'functionCall' => [MathTrig::class, 'PRODUCT'],
1510
            'argumentCount' => '1+',
1511
        ],
1512
        'PROPER' => [
1513
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1514
            'functionCall' => [TextData::class, 'PROPERCASE'],
1515
            'argumentCount' => '1',
1516
        ],
1517
        'PV' => [
1518
            'category' => Category::CATEGORY_FINANCIAL,
1519
            'functionCall' => [Financial::class, 'PV'],
1520
            'argumentCount' => '3-5',
1521
        ],
1522
        'QUARTILE' => [
1523
            'category' => Category::CATEGORY_STATISTICAL,
1524
            'functionCall' => [Statistical::class, 'QUARTILE'],
1525
            'argumentCount' => '2',
1526
        ],
1527
        'QUOTIENT' => [
1528
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1529
            'functionCall' => [MathTrig::class, 'QUOTIENT'],
1530
            'argumentCount' => '2',
1531
        ],
1532
        'RADIANS' => [
1533
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1534
            'functionCall' => 'deg2rad',
1535
            'argumentCount' => '1',
1536
        ],
1537
        'RAND' => [
1538
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1539
            'functionCall' => [MathTrig::class, 'RAND'],
1540
            'argumentCount' => '0',
1541
        ],
1542
        'RANDBETWEEN' => [
1543
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1544
            'functionCall' => [MathTrig::class, 'RAND'],
1545
            'argumentCount' => '2',
1546
        ],
1547
        'RANK' => [
1548
            'category' => Category::CATEGORY_STATISTICAL,
1549
            'functionCall' => [Statistical::class, 'RANK'],
1550
            'argumentCount' => '2,3',
1551
        ],
1552
        'RATE' => [
1553
            'category' => Category::CATEGORY_FINANCIAL,
1554
            'functionCall' => [Financial::class, 'RATE'],
1555
            'argumentCount' => '3-6',
1556
        ],
1557
        'RECEIVED' => [
1558
            'category' => Category::CATEGORY_FINANCIAL,
1559
            'functionCall' => [Financial::class, 'RECEIVED'],
1560
            'argumentCount' => '4-5',
1561
        ],
1562
        'REPLACE' => [
1563
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1564
            'functionCall' => [TextData::class, 'REPLACE'],
1565
            'argumentCount' => '4',
1566
        ],
1567
        'REPLACEB' => [
1568
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1569
            'functionCall' => [TextData::class, 'REPLACE'],
1570
            'argumentCount' => '4',
1571
        ],
1572
        'REPT' => [
1573
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1574
            'functionCall' => 'str_repeat',
1575
            'argumentCount' => '2',
1576
        ],
1577
        'RIGHT' => [
1578
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1579
            'functionCall' => [TextData::class, 'RIGHT'],
1580
            'argumentCount' => '1,2',
1581
        ],
1582
        'RIGHTB' => [
1583
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1584
            'functionCall' => [TextData::class, 'RIGHT'],
1585
            'argumentCount' => '1,2',
1586
        ],
1587
        'ROMAN' => [
1588
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1589
            'functionCall' => [MathTrig::class, 'ROMAN'],
1590
            'argumentCount' => '1,2',
1591
        ],
1592
        'ROUND' => [
1593
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1594
            'functionCall' => 'round',
1595
            'argumentCount' => '2',
1596
        ],
1597
        'ROUNDDOWN' => [
1598
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1599
            'functionCall' => [MathTrig::class, 'ROUNDDOWN'],
1600
            'argumentCount' => '2',
1601
        ],
1602
        'ROUNDUP' => [
1603
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1604
            'functionCall' => [MathTrig::class, 'ROUNDUP'],
1605
            'argumentCount' => '2',
1606
        ],
1607
        'ROW' => [
1608
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1609
            'functionCall' => [LookupRef::class, 'ROW'],
1610
            'argumentCount' => '-1',
1611
            'passByReference' => [true],
1612
        ],
1613
        'ROWS' => [
1614
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1615
            'functionCall' => [LookupRef::class, 'ROWS'],
1616
            'argumentCount' => '1',
1617
        ],
1618
        'RSQ' => [
1619
            'category' => Category::CATEGORY_STATISTICAL,
1620
            'functionCall' => [Statistical::class, 'RSQ'],
1621
            'argumentCount' => '2',
1622
        ],
1623
        'RTD' => [
1624
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1625
            'functionCall' => [Functions::class, 'DUMMY'],
1626
            'argumentCount' => '1+',
1627
        ],
1628
        'SEARCH' => [
1629
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1630
            'functionCall' => [TextData::class, 'SEARCHINSENSITIVE'],
1631
            'argumentCount' => '2,3',
1632
        ],
1633
        'SEARCHB' => [
1634
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1635
            'functionCall' => [TextData::class, 'SEARCHINSENSITIVE'],
1636
            'argumentCount' => '2,3',
1637
        ],
1638
        'SECOND' => [
1639
            'category' => Category::CATEGORY_DATE_AND_TIME,
1640
            'functionCall' => [DateTime::class, 'SECOND'],
1641
            'argumentCount' => '1',
1642
        ],
1643
        'SERIESSUM' => [
1644
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1645
            'functionCall' => [MathTrig::class, 'SERIESSUM'],
1646
            'argumentCount' => '4',
1647
        ],
1648
        'SIGN' => [
1649
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1650
            'functionCall' => [MathTrig::class, 'SIGN'],
1651
            'argumentCount' => '1',
1652
        ],
1653
        'SIN' => [
1654
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1655
            'functionCall' => 'sin',
1656
            'argumentCount' => '1',
1657
        ],
1658
        'SINH' => [
1659
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1660
            'functionCall' => 'sinh',
1661
            'argumentCount' => '1',
1662
        ],
1663
        'SKEW' => [
1664
            'category' => Category::CATEGORY_STATISTICAL,
1665
            'functionCall' => [Statistical::class, 'SKEW'],
1666
            'argumentCount' => '1+',
1667
        ],
1668
        'SLN' => [
1669
            'category' => Category::CATEGORY_FINANCIAL,
1670
            'functionCall' => [Financial::class, 'SLN'],
1671
            'argumentCount' => '3',
1672
        ],
1673
        'SLOPE' => [
1674
            'category' => Category::CATEGORY_STATISTICAL,
1675
            'functionCall' => [Statistical::class, 'SLOPE'],
1676
            'argumentCount' => '2',
1677
        ],
1678
        'SMALL' => [
1679
            'category' => Category::CATEGORY_STATISTICAL,
1680
            'functionCall' => [Statistical::class, 'SMALL'],
1681
            'argumentCount' => '2',
1682
        ],
1683
        'SQRT' => [
1684
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1685
            'functionCall' => 'sqrt',
1686
            'argumentCount' => '1',
1687
        ],
1688
        'SQRTPI' => [
1689
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1690
            'functionCall' => [MathTrig::class, 'SQRTPI'],
1691
            'argumentCount' => '1',
1692
        ],
1693
        'STANDARDIZE' => [
1694
            'category' => Category::CATEGORY_STATISTICAL,
1695
            'functionCall' => [Statistical::class, 'STANDARDIZE'],
1696
            'argumentCount' => '3',
1697
        ],
1698
        'STDEV' => [
1699
            'category' => Category::CATEGORY_STATISTICAL,
1700
            'functionCall' => [Statistical::class, 'STDEV'],
1701
            'argumentCount' => '1+',
1702
        ],
1703
        'STDEVA' => [
1704
            'category' => Category::CATEGORY_STATISTICAL,
1705
            'functionCall' => [Statistical::class, 'STDEVA'],
1706
            'argumentCount' => '1+',
1707
        ],
1708
        'STDEVP' => [
1709
            'category' => Category::CATEGORY_STATISTICAL,
1710
            'functionCall' => [Statistical::class, 'STDEVP'],
1711
            'argumentCount' => '1+',
1712
        ],
1713
        'STDEVPA' => [
1714
            'category' => Category::CATEGORY_STATISTICAL,
1715
            'functionCall' => [Statistical::class, 'STDEVPA'],
1716
            'argumentCount' => '1+',
1717
        ],
1718
        'STEYX' => [
1719
            'category' => Category::CATEGORY_STATISTICAL,
1720
            'functionCall' => [Statistical::class, 'STEYX'],
1721
            'argumentCount' => '2',
1722
        ],
1723
        'SUBSTITUTE' => [
1724
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1725
            'functionCall' => [TextData::class, 'SUBSTITUTE'],
1726
            'argumentCount' => '3,4',
1727
        ],
1728
        'SUBTOTAL' => [
1729
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1730
            'functionCall' => [MathTrig::class, 'SUBTOTAL'],
1731
            'argumentCount' => '2+',
1732
            'passCellReference' => true,
1733
        ],
1734
        'SUM' => [
1735
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1736
            'functionCall' => [MathTrig::class, 'SUM'],
1737
            'argumentCount' => '1+',
1738
        ],
1739
        'SUMIF' => [
1740
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1741
            'functionCall' => [MathTrig::class, 'SUMIF'],
1742
            'argumentCount' => '2,3',
1743
        ],
1744
        'SUMIFS' => [
1745
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1746
            'functionCall' => [MathTrig::class, 'SUMIFS'],
1747
            'argumentCount' => '3+',
1748
        ],
1749
        'SUMPRODUCT' => [
1750
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1751
            'functionCall' => [MathTrig::class, 'SUMPRODUCT'],
1752
            'argumentCount' => '1+',
1753
        ],
1754
        'SUMSQ' => [
1755
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1756
            'functionCall' => [MathTrig::class, 'SUMSQ'],
1757
            'argumentCount' => '1+',
1758
        ],
1759
        'SUMX2MY2' => [
1760
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1761
            'functionCall' => [MathTrig::class, 'SUMX2MY2'],
1762
            'argumentCount' => '2',
1763
        ],
1764
        'SUMX2PY2' => [
1765
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1766
            'functionCall' => [MathTrig::class, 'SUMX2PY2'],
1767
            'argumentCount' => '2',
1768
        ],
1769
        'SUMXMY2' => [
1770
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1771
            'functionCall' => [MathTrig::class, 'SUMXMY2'],
1772
            'argumentCount' => '2',
1773
        ],
1774
        'SYD' => [
1775
            'category' => Category::CATEGORY_FINANCIAL,
1776
            'functionCall' => [Financial::class, 'SYD'],
1777
            'argumentCount' => '4',
1778
        ],
1779
        'T' => [
1780
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1781
            'functionCall' => [TextData::class, 'RETURNSTRING'],
1782
            'argumentCount' => '1',
1783
        ],
1784
        'TAN' => [
1785
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1786
            'functionCall' => 'tan',
1787
            'argumentCount' => '1',
1788
        ],
1789
        'TANH' => [
1790
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1791
            'functionCall' => 'tanh',
1792
            'argumentCount' => '1',
1793
        ],
1794
        'TBILLEQ' => [
1795
            'category' => Category::CATEGORY_FINANCIAL,
1796
            'functionCall' => [Financial::class, 'TBILLEQ'],
1797
            'argumentCount' => '3',
1798
        ],
1799
        'TBILLPRICE' => [
1800
            'category' => Category::CATEGORY_FINANCIAL,
1801
            'functionCall' => [Financial::class, 'TBILLPRICE'],
1802
            'argumentCount' => '3',
1803
        ],
1804
        'TBILLYIELD' => [
1805
            'category' => Category::CATEGORY_FINANCIAL,
1806
            'functionCall' => [Financial::class, 'TBILLYIELD'],
1807
            'argumentCount' => '3',
1808
        ],
1809
        'TDIST' => [
1810
            'category' => Category::CATEGORY_STATISTICAL,
1811
            'functionCall' => [Statistical::class, 'TDIST'],
1812
            'argumentCount' => '3',
1813
        ],
1814
        'TEXT' => [
1815
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1816
            'functionCall' => [TextData::class, 'TEXTFORMAT'],
1817
            'argumentCount' => '2',
1818
        ],
1819
        'TIME' => [
1820
            'category' => Category::CATEGORY_DATE_AND_TIME,
1821
            'functionCall' => [DateTime::class, 'TIME'],
1822
            'argumentCount' => '3',
1823
        ],
1824
        'TIMEVALUE' => [
1825
            'category' => Category::CATEGORY_DATE_AND_TIME,
1826
            'functionCall' => [DateTime::class, 'TIMEVALUE'],
1827
            'argumentCount' => '1',
1828
        ],
1829
        'TINV' => [
1830
            'category' => Category::CATEGORY_STATISTICAL,
1831
            'functionCall' => [Statistical::class, 'TINV'],
1832
            'argumentCount' => '2',
1833
        ],
1834
        'TODAY' => [
1835
            'category' => Category::CATEGORY_DATE_AND_TIME,
1836
            'functionCall' => [DateTime::class, 'DATENOW'],
1837
            'argumentCount' => '0',
1838
        ],
1839
        'TRANSPOSE' => [
1840
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1841
            'functionCall' => [LookupRef::class, 'TRANSPOSE'],
1842
            'argumentCount' => '1',
1843
        ],
1844
        'TREND' => [
1845
            'category' => Category::CATEGORY_STATISTICAL,
1846
            'functionCall' => [Statistical::class, 'TREND'],
1847
            'argumentCount' => '1-4',
1848
        ],
1849
        'TRIM' => [
1850
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1851
            'functionCall' => [TextData::class, 'TRIMSPACES'],
1852
            'argumentCount' => '1',
1853
        ],
1854
        'TRIMMEAN' => [
1855
            'category' => Category::CATEGORY_STATISTICAL,
1856
            'functionCall' => [Statistical::class, 'TRIMMEAN'],
1857
            'argumentCount' => '2',
1858
        ],
1859
        'TRUE' => [
1860
            'category' => Category::CATEGORY_LOGICAL,
1861
            'functionCall' => [Logical::class, 'TRUE'],
1862
            'argumentCount' => '0',
1863
        ],
1864
        'TRUNC' => [
1865
            'category' => Category::CATEGORY_MATH_AND_TRIG,
1866
            'functionCall' => [MathTrig::class, 'TRUNC'],
1867
            'argumentCount' => '1,2',
1868
        ],
1869
        'TTEST' => [
1870
            'category' => Category::CATEGORY_STATISTICAL,
1871
            'functionCall' => [Functions::class, 'DUMMY'],
1872
            'argumentCount' => '4',
1873
        ],
1874
        'TYPE' => [
1875
            'category' => Category::CATEGORY_INFORMATION,
1876
            'functionCall' => [Functions::class, 'TYPE'],
1877
            'argumentCount' => '1',
1878
        ],
1879
        'UPPER' => [
1880
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1881
            'functionCall' => [TextData::class, 'UPPERCASE'],
1882
            'argumentCount' => '1',
1883
        ],
1884
        'USDOLLAR' => [
1885
            'category' => Category::CATEGORY_FINANCIAL,
1886
            'functionCall' => [Functions::class, 'DUMMY'],
1887
            'argumentCount' => '2',
1888
        ],
1889
        'VALUE' => [
1890
            'category' => Category::CATEGORY_TEXT_AND_DATA,
1891
            'functionCall' => [TextData::class, 'VALUE'],
1892
            'argumentCount' => '1',
1893
        ],
1894
        'VAR' => [
1895
            'category' => Category::CATEGORY_STATISTICAL,
1896
            'functionCall' => [Statistical::class, 'VARFunc'],
1897
            'argumentCount' => '1+',
1898
        ],
1899
        'VARA' => [
1900
            'category' => Category::CATEGORY_STATISTICAL,
1901
            'functionCall' => [Statistical::class, 'VARA'],
1902
            'argumentCount' => '1+',
1903
        ],
1904
        'VARP' => [
1905
            'category' => Category::CATEGORY_STATISTICAL,
1906
            'functionCall' => [Statistical::class, 'VARP'],
1907
            'argumentCount' => '1+',
1908
        ],
1909
        'VARPA' => [
1910
            'category' => Category::CATEGORY_STATISTICAL,
1911
            'functionCall' => [Statistical::class, 'VARPA'],
1912
            'argumentCount' => '1+',
1913
        ],
1914
        'VDB' => [
1915
            'category' => Category::CATEGORY_FINANCIAL,
1916
            'functionCall' => [Functions::class, 'DUMMY'],
1917
            'argumentCount' => '5-7',
1918
        ],
1919
        'VLOOKUP' => [
1920
            'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1921
            'functionCall' => [LookupRef::class, 'VLOOKUP'],
1922
            'argumentCount' => '3,4',
1923
        ],
1924
        'WEEKDAY' => [
1925
            'category' => Category::CATEGORY_DATE_AND_TIME,
1926
            'functionCall' => [DateTime::class, 'WEEKDAY'],
1927
            'argumentCount' => '1,2',
1928
        ],
1929
        'WEEKNUM' => [
1930
            'category' => Category::CATEGORY_DATE_AND_TIME,
1931
            'functionCall' => [DateTime::class, 'WEEKNUM'],
1932
            'argumentCount' => '1,2',
1933
        ],
1934
        'WEIBULL' => [
1935
            'category' => Category::CATEGORY_STATISTICAL,
1936
            'functionCall' => [Statistical::class, 'WEIBULL'],
1937
            'argumentCount' => '4',
1938
        ],
1939
        'WORKDAY' => [
1940
            'category' => Category::CATEGORY_DATE_AND_TIME,
1941
            'functionCall' => [DateTime::class, 'WORKDAY'],
1942
            'argumentCount' => '2+',
1943
        ],
1944
        'XIRR' => [
1945
            'category' => Category::CATEGORY_FINANCIAL,
1946
            'functionCall' => [Financial::class, 'XIRR'],
1947
            'argumentCount' => '2,3',
1948
        ],
1949
        'XNPV' => [
1950
            'category' => Category::CATEGORY_FINANCIAL,
1951
            'functionCall' => [Financial::class, 'XNPV'],
1952
            'argumentCount' => '3',
1953
        ],
1954
        'YEAR' => [
1955
            'category' => Category::CATEGORY_DATE_AND_TIME,
1956
            'functionCall' => [DateTime::class, 'YEAR'],
1957
            'argumentCount' => '1',
1958
        ],
1959
        'YEARFRAC' => [
1960
            'category' => Category::CATEGORY_DATE_AND_TIME,
1961
            'functionCall' => [DateTime::class, 'YEARFRAC'],
1962
            'argumentCount' => '2,3',
1963
        ],
1964
        'YIELD' => [
1965
            'category' => Category::CATEGORY_FINANCIAL,
1966
            'functionCall' => [Functions::class, 'DUMMY'],
1967
            'argumentCount' => '6,7',
1968
        ],
1969
        'YIELDDISC' => [
1970
            'category' => Category::CATEGORY_FINANCIAL,
1971
            'functionCall' => [Financial::class, 'YIELDDISC'],
1972
            'argumentCount' => '4,5',
1973
        ],
1974
        'YIELDMAT' => [
1975
            'category' => Category::CATEGORY_FINANCIAL,
1976
            'functionCall' => [Financial::class, 'YIELDMAT'],
1977
            'argumentCount' => '5,6',
1978
        ],
1979
        'ZTEST' => [
1980
            'category' => Category::CATEGORY_STATISTICAL,
1981
            'functionCall' => [Statistical::class, 'ZTEST'],
1982
            'argumentCount' => '2-3',
1983
        ],
1984
    ];
1985
1986
    //    Internal functions used for special control purposes
1987
    private static $controlFunctions = [
1988
        'MKMATRIX' => [
1989
            'argumentCount' => '*',
1990
            'functionCall' => 'self::mkMatrix',
1991
        ],
1992
    ];
1993
1994 158
    public function __construct(Spreadsheet $spreadsheet = null)
1995
    {
1996 158
        $this->delta = 1 * pow(10, 0 - ini_get('precision'));
1997
1998 158
        $this->spreadsheet = $spreadsheet;
1999 158
        $this->cyclicReferenceStack = new CyclicReferenceStack();
2000 158
        $this->debugLog = new Logger($this->cyclicReferenceStack);
2001 158
    }
2002
2003 1
    private static function loadLocales()
2004
    {
2005 1
        $localeFileDirectory = __DIR__ . '/locale/';
2006 1
        foreach (glob($localeFileDirectory . '/*', GLOB_ONLYDIR) as $filename) {
2007 1
            $filename = substr($filename, strlen($localeFileDirectory) + 1);
2008 1
            if ($filename != 'en') {
2009 1
                self::$validLocaleLanguages[] = $filename;
2010
            }
2011
        }
2012 1
    }
2013
2014
    /**
2015
     * Get an instance of this class.
2016
     *
2017
     * @param Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object,
2018
     *                                    or NULL to create a standalone claculation engine
2019
     *
2020
     * @return Calculation
2021
     */
2022 534
    public static function getInstance(Spreadsheet $spreadsheet = null)
2023
    {
2024 534
        if ($spreadsheet !== null) {
2025 103
            $instance = $spreadsheet->getCalculationEngine();
2026 103
            if (isset($instance)) {
2027 103
                return $instance;
2028
            }
2029
        }
2030
2031 445
        if (!isset(self::$instance) || (self::$instance === null)) {
2032 14
            self::$instance = new self();
2033
        }
2034
2035 445
        return self::$instance;
2036
    }
2037
2038
    /**
2039
     * Flush the calculation cache for any existing instance of this class
2040
     *        but only if a Calculation instance exists.
2041
     */
2042
    public function flushInstance()
2043
    {
2044
        $this->clearCalculationCache();
2045
    }
2046
2047
    /**
2048
     * Get the Logger for this calculation engine instance.
2049
     *
2050
     * @return Logger
2051
     */
2052 74
    public function getDebugLog()
2053
    {
2054 74
        return $this->debugLog;
2055
    }
2056
2057
    /**
2058
     * __clone implementation. Cloning should not be allowed in a Singleton!
2059
     *
2060
     * @throws Exception
2061
     */
2062
    final public function __clone()
2063
    {
2064
        throw new Exception('Cloning the calculation engine is not allowed!');
2065
    }
2066
2067
    /**
2068
     * Return the locale-specific translation of TRUE.
2069
     *
2070
     * @return string locale-specific translation of TRUE
2071
     */
2072 37
    public static function getTRUE()
2073
    {
2074 37
        return self::$localeBoolean['TRUE'];
2075
    }
2076
2077
    /**
2078
     * Return the locale-specific translation of FALSE.
2079
     *
2080
     * @return string locale-specific translation of FALSE
2081
     */
2082 30
    public static function getFALSE()
2083
    {
2084 30
        return self::$localeBoolean['FALSE'];
2085
    }
2086
2087
    /**
2088
     * Set the Array Return Type (Array or Value of first element in the array).
2089
     *
2090
     * @param string $returnType Array return type
2091
     *
2092
     * @return bool Success or failure
2093
     */
2094 26
    public static function setArrayReturnType($returnType)
2095
    {
2096 26
        if (($returnType == self::RETURN_ARRAY_AS_VALUE) ||
2097 19
            ($returnType == self::RETURN_ARRAY_AS_ERROR) ||
2098 26
            ($returnType == self::RETURN_ARRAY_AS_ARRAY)) {
2099 26
            self::$returnArrayAsType = $returnType;
2100
2101 26
            return true;
2102
        }
2103
2104
        return false;
2105
    }
2106
2107
    /**
2108
     * Return the Array Return Type (Array or Value of first element in the array).
2109
     *
2110
     * @return string $returnType Array return type
2111
     */
2112 15
    public static function getArrayReturnType()
2113
    {
2114 15
        return self::$returnArrayAsType;
2115
    }
2116
2117
    /**
2118
     * Is calculation caching enabled?
2119
     *
2120
     * @return bool
2121
     */
2122 1
    public function getCalculationCacheEnabled()
2123
    {
2124 1
        return $this->calculationCacheEnabled;
2125
    }
2126
2127
    /**
2128
     * Enable/disable calculation cache.
2129
     *
2130
     * @param bool $pValue
2131
     */
2132
    public function setCalculationCacheEnabled($pValue)
2133
    {
2134
        $this->calculationCacheEnabled = $pValue;
2135
        $this->clearCalculationCache();
2136
    }
2137
2138
    /**
2139
     * Enable calculation cache.
2140
     */
2141
    public function enableCalculationCache()
2142
    {
2143
        $this->setCalculationCacheEnabled(true);
2144
    }
2145
2146
    /**
2147
     * Disable calculation cache.
2148
     */
2149
    public function disableCalculationCache()
2150
    {
2151
        $this->setCalculationCacheEnabled(false);
2152
    }
2153
2154
    /**
2155
     * Clear calculation cache.
2156
     */
2157
    public function clearCalculationCache()
2158
    {
2159
        $this->calculationCache = [];
2160
    }
2161
2162
    /**
2163
     * Clear calculation cache for a specified worksheet.
2164
     *
2165
     * @param string $worksheetName
2166
     */
2167 3
    public function clearCalculationCacheForWorksheet($worksheetName)
2168
    {
2169 3
        if (isset($this->calculationCache[$worksheetName])) {
2170
            unset($this->calculationCache[$worksheetName]);
2171
        }
2172 3
    }
2173
2174
    /**
2175
     * Rename calculation cache for a specified worksheet.
2176
     *
2177
     * @param string $fromWorksheetName
2178
     * @param string $toWorksheetName
2179
     */
2180 158
    public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName)
2181
    {
2182 158
        if (isset($this->calculationCache[$fromWorksheetName])) {
2183
            $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
2184
            unset($this->calculationCache[$fromWorksheetName]);
2185
        }
2186 158
    }
2187
2188
    /**
2189
     * Get the currently defined locale code.
2190
     *
2191
     * @return string
2192
     */
2193
    public function getLocale()
2194
    {
2195
        return self::$localeLanguage;
2196
    }
2197
2198
    /**
2199
     * Set the locale code.
2200
     *
2201
     * @param string $locale The locale to use for formula translation, eg: 'en_us'
2202
     *
2203
     * @return bool
2204
     */
2205 426
    public function setLocale($locale)
2206
    {
2207
        //    Identify our locale and language
2208 426
        $language = $locale = strtolower($locale);
2209 426
        if (strpos($locale, '_') !== false) {
2210 426
            list($language) = explode('_', $locale);
2211
        }
2212
2213 426
        if (count(self::$validLocaleLanguages) == 1) {
2214 1
            self::loadLocales();
2215
        }
2216
        //    Test whether we have any language data for this language (any locale)
2217 426
        if (in_array($language, self::$validLocaleLanguages)) {
2218
            //    initialise language/locale settings
2219 426
            self::$localeFunctions = [];
2220 426
            self::$localeArgumentSeparator = ',';
2221 426
            self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL'];
2222
            //    Default is English, if user isn't requesting english, then read the necessary data from the locale files
2223 426
            if ($locale != 'en_us') {
2224
                //    Search for a file with a list of function names for locale
2225 17
                $functionNamesFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'functions';
2226 17
                if (!file_exists($functionNamesFile)) {
2227
                    //    If there isn't a locale specific function file, look for a language specific function file
2228
                    $functionNamesFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'functions';
2229
                    if (!file_exists($functionNamesFile)) {
2230
                        return false;
2231
                    }
2232
                }
2233
                //    Retrieve the list of locale or language specific function names
2234 17
                $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2235 17
                foreach ($localeFunctions as $localeFunction) {
2236 17
                    list($localeFunction) = explode('##', $localeFunction); //    Strip out comments
2237 17
                    if (strpos($localeFunction, '=') !== false) {
2238 17
                        list($fName, $lfName) = explode('=', $localeFunction);
2239 17
                        $fName = trim($fName);
2240 17
                        $lfName = trim($lfName);
2241 17
                        if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) {
2242 17
                            self::$localeFunctions[$fName] = $lfName;
2243
                        }
2244
                    }
2245
                }
2246
                //    Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions
2247 17
                if (isset(self::$localeFunctions['TRUE'])) {
2248 17
                    self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE'];
2249
                }
2250 17
                if (isset(self::$localeFunctions['FALSE'])) {
2251 17
                    self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE'];
2252
                }
2253
2254 17
                $configFile = __DIR__ . '/locale/' . str_replace('_', DIRECTORY_SEPARATOR, $locale) . DIRECTORY_SEPARATOR . 'config';
2255 17
                if (!file_exists($configFile)) {
2256
                    $configFile = __DIR__ . '/locale/' . $language . DIRECTORY_SEPARATOR . 'config';
2257
                }
2258 17
                if (file_exists($configFile)) {
2259 17
                    $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2260 17
                    foreach ($localeSettings as $localeSetting) {
2261 17
                        list($localeSetting) = explode('##', $localeSetting); //    Strip out comments
2262 17
                        if (strpos($localeSetting, '=') !== false) {
2263 17
                            list($settingName, $settingValue) = explode('=', $localeSetting);
2264 17
                            $settingName = strtoupper(trim($settingName));
2265
                            switch ($settingName) {
2266 17
                                case 'ARGUMENTSEPARATOR':
2267 17
                                    self::$localeArgumentSeparator = trim($settingValue);
2268
2269 17
                                    break;
2270
                            }
2271
                        }
2272
                    }
2273
                }
2274
            }
2275
2276
            self::$functionReplaceFromExcel = self::$functionReplaceToExcel =
2277 426
            self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
2278 426
            self::$localeLanguage = $locale;
2279
2280 426
            return true;
2281
        }
2282
2283
        return false;
2284
    }
2285
2286
    /**
2287
     * @param string $fromSeparator
2288
     * @param string $toSeparator
2289
     * @param string $formula
2290
     * @param bool $inBraces
2291
     *
2292
     * @return string
2293
     */
2294 5
    public static function translateSeparator($fromSeparator, $toSeparator, $formula, &$inBraces)
2295
    {
2296 5
        $strlen = mb_strlen($formula);
2297 5
        for ($i = 0; $i < $strlen; ++$i) {
2298 5
            $chr = mb_substr($formula, $i, 1);
2299
            switch ($chr) {
2300 5
                case '{':
2301
                    $inBraces = true;
2302
2303
                    break;
2304 5
                case '}':
2305
                    $inBraces = false;
2306
2307
                    break;
2308 5
                case $fromSeparator:
2309
                    if (!$inBraces) {
2310
                        $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
2311
                    }
2312
            }
2313
        }
2314
2315 5
        return $formula;
2316
    }
2317
2318
    /**
2319
     * @param string[] $from
2320
     * @param string[] $to
2321
     * @param string $formula
2322
     * @param string $fromSeparator
2323
     * @param string $toSeparator
2324
     *
2325
     * @return string
2326
     */
2327
    private static function translateFormula(array $from, array $to, $formula, $fromSeparator, $toSeparator)
2328
    {
2329
        //    Convert any Excel function names to the required language
2330
        if (self::$localeLanguage !== 'en_us') {
2331
            $inBraces = false;
2332
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
2333
            if (strpos($formula, '"') !== false) {
2334
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
2335
                //        the formula
2336
                $temp = explode('"', $formula);
2337
                $i = false;
2338
                foreach ($temp as &$value) {
2339
                    //    Only count/replace in alternating array entries
2340
                    if ($i = !$i) {
2341
                        $value = preg_replace($from, $to, $value);
2342
                        $value = self::translateSeparator($fromSeparator, $toSeparator, $value, $inBraces);
2343
                    }
2344
                }
2345
                unset($value);
2346
                //    Then rebuild the formula string
2347
                $formula = implode('"', $temp);
2348
            } else {
2349
                //    If there's no quoted strings, then we do a simple count/replace
2350
                $formula = preg_replace($from, $to, $formula);
2351
                $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inBraces);
2352
            }
2353
        }
2354
2355
        return $formula;
2356
    }
2357
2358
    private static $functionReplaceFromExcel = null;
2359
2360
    private static $functionReplaceToLocale = null;
2361
2362
    public function _translateFormulaToLocale($formula)
2363
    {
2364
        if (self::$functionReplaceFromExcel === null) {
2365
            self::$functionReplaceFromExcel = [];
2366
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2367
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/Ui';
2368
            }
2369
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2370
                self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui';
2371
            }
2372
        }
2373
2374
        if (self::$functionReplaceToLocale === null) {
2375
            self::$functionReplaceToLocale = [];
2376
            foreach (array_values(self::$localeFunctions) as $localeFunctionName) {
2377
                self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2';
2378
            }
2379
            foreach (array_values(self::$localeBoolean) as $localeBoolean) {
2380
                self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2';
2381
            }
2382
        }
2383
2384
        return self::translateFormula(self::$functionReplaceFromExcel, self::$functionReplaceToLocale, $formula, ',', self::$localeArgumentSeparator);
2385
    }
2386
2387
    private static $functionReplaceFromLocale = null;
2388
2389
    private static $functionReplaceToExcel = null;
2390
2391
    public function _translateFormulaToEnglish($formula)
2392
    {
2393
        if (self::$functionReplaceFromLocale === null) {
2394
            self::$functionReplaceFromLocale = [];
2395
            foreach (array_values(self::$localeFunctions) as $localeFunctionName) {
2396
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/Ui';
2397
            }
2398
            foreach (array_values(self::$localeBoolean) as $excelBoolean) {
2399
                self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui';
2400
            }
2401
        }
2402
2403
        if (self::$functionReplaceToExcel === null) {
2404
            self::$functionReplaceToExcel = [];
2405
            foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
2406
                self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2';
2407
            }
2408
            foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
2409
                self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2';
2410
            }
2411
        }
2412
2413
        return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ',');
2414
    }
2415
2416 46
    public static function localeFunc($function)
2417
    {
2418 46
        if (self::$localeLanguage !== 'en_us') {
2419
            $functionName = trim($function, '(');
2420
            if (isset(self::$localeFunctions[$functionName])) {
2421
                $brace = ($functionName != $function);
2422
                $function = self::$localeFunctions[$functionName];
2423
                if ($brace) {
2424
                    $function .= '(';
2425
                }
2426
            }
2427
        }
2428
2429 46
        return $function;
2430
    }
2431
2432
    /**
2433
     * Wrap string values in quotes.
2434
     *
2435
     * @param mixed $value
2436
     *
2437
     * @return mixed
2438
     */
2439 80
    public static function wrapResult($value)
2440
    {
2441 80
        if (is_string($value)) {
2442
            //    Error values cannot be "wrapped"
2443 68
            if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
2444
                //    Return Excel errors "as is"
2445 10
                return $value;
2446
            }
2447
            //    Return strings wrapped in quotes
2448 59
            return '"' . $value . '"';
2449
        //    Convert numeric errors to NaN error
2450 44
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2451
            return Functions::NAN();
2452
        }
2453
2454 44
        return $value;
2455
    }
2456
2457
    /**
2458
     * Remove quotes used as a wrapper to identify string values.
2459
     *
2460
     * @param mixed $value
2461
     *
2462
     * @return mixed
2463
     */
2464 94
    public static function unwrapResult($value)
2465
    {
2466 94
        if (is_string($value)) {
2467 73
            if ((isset($value[0])) && ($value[0] == '"') && (substr($value, -1) == '"')) {
2468 73
                return substr($value, 1, -1);
2469
            }
2470
            //    Convert numeric errors to NAN error
2471 59
        } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
2472
            return Functions::NAN();
2473
        }
2474
2475 60
        return $value;
2476
    }
2477
2478
    /**
2479
     * Calculate cell value (using formula from a cell ID)
2480
     * Retained for backward compatibility.
2481
     *
2482
     * @param Cell $pCell Cell to calculate
2483
     *
2484
     * @throws Exception
2485
     *
2486
     * @return mixed
2487
     */
2488
    public function calculate(Cell $pCell = null)
2489
    {
2490
        try {
2491
            return $this->calculateCellValue($pCell);
2492
        } catch (\Exception $e) {
2493
            throw new Exception($e->getMessage());
2494
        }
2495
    }
2496
2497
    /**
2498
     * Calculate the value of a cell formula.
2499
     *
2500
     * @param Cell $pCell Cell to calculate
2501
     * @param bool $resetLog Flag indicating whether the debug log should be reset or not
2502
     *
2503
     * @throws Exception
2504
     *
2505
     * @return mixed
2506
     */
2507 48
    public function calculateCellValue(Cell $pCell = null, $resetLog = true)
2508
    {
2509 48
        if ($pCell === null) {
2510
            return null;
2511
        }
2512
2513 48
        $returnArrayAsType = self::$returnArrayAsType;
2514 48
        if ($resetLog) {
2515
            //    Initialise the logging settings if requested
2516 47
            $this->formulaError = null;
2517 47
            $this->debugLog->clearLog();
2518 47
            $this->cyclicReferenceStack->clear();
2519 47
            $this->cyclicFormulaCounter = 1;
2520
2521 47
            self::$returnArrayAsType = self::RETURN_ARRAY_AS_ARRAY;
2522
        }
2523
2524
        //    Execute the calculation for the cell formula
2525 48
        $this->cellStack[] = [
2526 48
            'sheet' => $pCell->getWorksheet()->getTitle(),
2527 48
            'cell' => $pCell->getCoordinate(),
2528
        ];
2529
2530
        try {
2531 48
            $result = self::unwrapResult($this->_calculateFormulaValue($pCell->getValue(), $pCell->getCoordinate(), $pCell));
2532 48
            $cellAddress = array_pop($this->cellStack);
2533 48
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2534
        } catch (\Exception $e) {
2535
            $cellAddress = array_pop($this->cellStack);
2536
            $this->spreadsheet->getSheetByName($cellAddress['sheet'])->getCell($cellAddress['cell']);
2537
2538
            throw new Exception($e->getMessage());
2539
        }
2540
2541 48
        if ((is_array($result)) && (self::$returnArrayAsType != self::RETURN_ARRAY_AS_ARRAY)) {
2542 2
            self::$returnArrayAsType = $returnArrayAsType;
2543 2
            $testResult = Functions::flattenArray($result);
2544 2
            if (self::$returnArrayAsType == self::RETURN_ARRAY_AS_ERROR) {
2545
                return Functions::VALUE();
2546
            }
2547
            //    If there's only a single cell in the array, then we allow it
2548 2
            if (count($testResult) != 1) {
2549
                //    If keys are numeric, then it's a matrix result rather than a cell range result, so we permit it
2550
                $r = array_keys($result);
2551
                $r = array_shift($r);
2552
                if (!is_numeric($r)) {
2553
                    return Functions::VALUE();
2554
                }
2555
                if (is_array($result[$r])) {
2556
                    $c = array_keys($result[$r]);
2557
                    $c = array_shift($c);
2558
                    if (!is_numeric($c)) {
2559
                        return Functions::VALUE();
2560
                    }
2561
                }
2562
            }
2563 2
            $result = array_shift($testResult);
2564
        }
2565 48
        self::$returnArrayAsType = $returnArrayAsType;
2566
2567 48
        if ($result === null) {
2568
            return 0;
2569 48
        } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
2570
            return Functions::NAN();
2571
        }
2572
2573 48
        return $result;
2574
    }
2575
2576
    /**
2577
     * Validate and parse a formula string.
2578
     *
2579
     * @param string $formula Formula to parse
2580
     *
2581
     * @return array
2582
     */
2583
    public function parseFormula($formula)
2584
    {
2585
        //    Basic validation that this is indeed a formula
2586
        //    We return an empty array if not
2587
        $formula = trim($formula);
2588
        if ((!isset($formula[0])) || ($formula[0] != '=')) {
2589
            return [];
2590
        }
2591
        $formula = ltrim(substr($formula, 1));
2592
        if (!isset($formula[0])) {
2593
            return [];
2594
        }
2595
2596
        //    Parse the formula and return the token stack
2597
        return $this->_parseFormula($formula);
2598
    }
2599
2600
    /**
2601
     * Calculate the value of a formula.
2602
     *
2603
     * @param string $formula Formula to parse
2604
     * @param string $cellID Address of the cell to calculate
2605
     * @param Cell $pCell Cell to calculate
2606
     *
2607
     * @throws Exception
2608
     *
2609
     * @return mixed
2610
     */
2611 1
    public function calculateFormula($formula, $cellID = null, Cell $pCell = null)
2612
    {
2613
        //    Initialise the logging settings
2614 1
        $this->formulaError = null;
2615 1
        $this->debugLog->clearLog();
2616 1
        $this->cyclicReferenceStack->clear();
2617
2618 1
        if ($this->spreadsheet !== null && $cellID === null && $pCell === null) {
2619
            $cellID = 'A1';
2620
            $pCell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
2621
        } else {
2622
            //    Disable calculation cacheing because it only applies to cell calculations, not straight formulae
2623
            //    But don't actually flush any cache
2624 1
            $resetCache = $this->getCalculationCacheEnabled();
2625 1
            $this->calculationCacheEnabled = false;
2626
        }
2627
2628
        //    Execute the calculation
2629
        try {
2630 1
            $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $pCell));
2631 1
        } catch (\Exception $e) {
2632 1
            throw new Exception($e->getMessage());
2633
        }
2634
2635 1
        if ($this->spreadsheet === null) {
2636
            //    Reset calculation cacheing to its previous state
2637
            $this->calculationCacheEnabled = $resetCache;
2638
        }
2639
2640 1
        return $result;
2641
    }
2642
2643
    /**
2644
     * @param string $cellReference
2645
     * @param mixed $cellValue
2646
     *
2647
     * @return bool
2648
     */
2649 49
    public function getValueFromCache($cellReference, &$cellValue)
2650
    {
2651
        // Is calculation cacheing enabled?
2652
        // Is the value present in calculation cache?
2653 49
        $this->debugLog->writeDebugLog('Testing cache value for cell ', $cellReference);
2654 49
        if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
2655 40
            $this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache');
2656
            // Return the cached result
2657 40
            $cellValue = $this->calculationCache[$cellReference];
2658
2659 40
            return true;
2660
        }
2661
2662 49
        return false;
2663
    }
2664
2665
    /**
2666
     * @param string $cellReference
2667
     * @param mixed $cellValue
2668
     */
2669 49
    public function saveValueToCache($cellReference, $cellValue)
2670
    {
2671 49
        if ($this->calculationCacheEnabled) {
2672 48
            $this->calculationCache[$cellReference] = $cellValue;
2673
        }
2674 49
    }
2675
2676
    /**
2677
     * Parse a cell formula and calculate its value.
2678
     *
2679
     * @param string $formula The formula to parse and calculate
2680
     * @param string $cellID The ID (e.g. A3) of the cell that we are calculating
2681
     * @param Cell $pCell Cell to calculate
2682
     *
2683
     * @throws Exception
2684
     *
2685
     * @return mixed
2686
     */
2687 118
    public function _calculateFormulaValue($formula, $cellID = null, Cell $pCell = null)
2688
    {
2689 118
        $cellValue = null;
2690
2691
        //    Basic validation that this is indeed a formula
2692
        //    We simply return the cell value if not
2693 118
        $formula = trim($formula);
2694 118
        if ($formula[0] != '=') {
2695
            return self::wrapResult($formula);
2696
        }
2697 118
        $formula = ltrim(substr($formula, 1));
2698 118
        if (!isset($formula[0])) {
2699
            return self::wrapResult($formula);
2700
        }
2701
2702 118
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
2703 118
        $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
2704 118
        $wsCellReference = $wsTitle . '!' . $cellID;
2705
2706 118
        if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
2707 40
            return $cellValue;
2708
        }
2709
2710 118
        if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) {
2711
            if ($this->cyclicFormulaCount <= 0) {
2712
                $this->cyclicFormulaCell = '';
2713
2714
                return $this->raiseFormulaError('Cyclic Reference in Formula');
2715
            } elseif ($this->cyclicFormulaCell === $wsCellReference) {
2716
                ++$this->cyclicFormulaCounter;
2717
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2718
                    $this->cyclicFormulaCell = '';
2719
2720
                    return $cellValue;
2721
                }
2722
            } elseif ($this->cyclicFormulaCell == '') {
2723
                if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
2724
                    return $cellValue;
2725
                }
2726
                $this->cyclicFormulaCell = $wsCellReference;
2727
            }
2728
        }
2729
2730
        //    Parse the formula onto the token stack and calculate the value
2731 118
        $this->cyclicReferenceStack->push($wsCellReference);
2732 118
        $cellValue = $this->processTokenStack($this->_parseFormula($formula, $pCell), $cellID, $pCell);
2733 118
        $this->cyclicReferenceStack->pop();
2734
2735
        // Save to calculation cache
2736 118
        if ($cellID !== null) {
2737 49
            $this->saveValueToCache($wsCellReference, $cellValue);
2738
        }
2739
2740
        //    Return the calculated value
2741 118
        return $cellValue;
2742
    }
2743
2744
    /**
2745
     * Ensure that paired matrix operands are both matrices and of the same size.
2746
     *
2747
     * @param mixed &$operand1 First matrix operand
2748
     * @param mixed &$operand2 Second matrix operand
2749
     * @param int $resize Flag indicating whether the matrices should be resized to match
2750
     *                                        and (if so), whether the smaller dimension should grow or the
2751
     *                                        larger should shrink.
2752
     *                                            0 = no resize
2753
     *                                            1 = shrink to fit
2754
     *                                            2 = extend to fit
2755
     *
2756
     * @return array
2757
     */
2758 5
    private static function checkMatrixOperands(&$operand1, &$operand2, $resize = 1)
2759
    {
2760
        //    Examine each of the two operands, and turn them into an array if they aren't one already
2761
        //    Note that this function should only be called if one or both of the operand is already an array
2762 5
        if (!is_array($operand1)) {
2763
            list($matrixRows, $matrixColumns) = self::getMatrixDimensions($operand2);
2764
            $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1));
2765
            $resize = 0;
2766 5
        } elseif (!is_array($operand2)) {
2767 2
            list($matrixRows, $matrixColumns) = self::getMatrixDimensions($operand1);
2768 2
            $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2));
2769 2
            $resize = 0;
2770
        }
2771
2772 5
        list($matrix1Rows, $matrix1Columns) = self::getMatrixDimensions($operand1);
2773 5
        list($matrix2Rows, $matrix2Columns) = self::getMatrixDimensions($operand2);
2774 5
        if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
2775 5
            $resize = 1;
2776
        }
2777
2778 5
        if ($resize == 2) {
2779
            //    Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger
2780
            self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2781 5
        } elseif ($resize == 1) {
2782
            //    Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller
2783 5
            self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
2784
        }
2785
2786 5
        return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns];
2787
    }
2788
2789
    /**
2790
     * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0.
2791
     *
2792
     * @param array &$matrix matrix operand
2793
     *
2794
     * @return int[] An array comprising the number of rows, and number of columns
2795
     */
2796 15
    public static function getMatrixDimensions(array &$matrix)
2797
    {
2798 15
        $matrixRows = count($matrix);
2799 15
        $matrixColumns = 0;
2800 15
        foreach ($matrix as $rowKey => $rowValue) {
2801 13
            if (!is_array($rowValue)) {
2802 4
                $matrix[$rowKey] = [$rowValue];
2803 4
                $matrixColumns = max(1, $matrixColumns);
2804
            } else {
2805 9
                $matrix[$rowKey] = array_values($rowValue);
2806 13
                $matrixColumns = max(count($rowValue), $matrixColumns);
2807
            }
2808
        }
2809 15
        $matrix = array_values($matrix);
2810
2811 15
        return [$matrixRows, $matrixColumns];
2812
    }
2813
2814
    /**
2815
     * Ensure that paired matrix operands are both matrices of the same size.
2816
     *
2817
     * @param mixed &$matrix1 First matrix operand
2818
     * @param mixed &$matrix2 Second matrix operand
2819
     * @param int $matrix1Rows Row size of first matrix operand
2820
     * @param int $matrix1Columns Column size of first matrix operand
2821
     * @param int $matrix2Rows Row size of second matrix operand
2822
     * @param int $matrix2Columns Column size of second matrix operand
2823
     */
2824 5
    private static function resizeMatricesShrink(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
2825
    {
2826 5
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
2827
            if ($matrix2Rows < $matrix1Rows) {
2828
                for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) {
2829
                    unset($matrix1[$i]);
2830
                }
2831
            }
2832
            if ($matrix2Columns < $matrix1Columns) {
2833
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2834
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
2835
                        unset($matrix1[$i][$j]);
2836
                    }
2837
                }
2838
            }
2839
        }
2840
2841 5
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
2842
            if ($matrix1Rows < $matrix2Rows) {
2843
                for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) {
2844
                    unset($matrix2[$i]);
2845
                }
2846
            }
2847
            if ($matrix1Columns < $matrix2Columns) {
2848
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2849
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
2850
                        unset($matrix2[$i][$j]);
2851
                    }
2852
                }
2853
            }
2854
        }
2855 5
    }
2856
2857
    /**
2858
     * Ensure that paired matrix operands are both matrices of the same size.
2859
     *
2860
     * @param mixed &$matrix1 First matrix operand
2861
     * @param mixed &$matrix2 Second matrix operand
2862
     * @param int $matrix1Rows Row size of first matrix operand
2863
     * @param int $matrix1Columns Column size of first matrix operand
2864
     * @param int $matrix2Rows Row size of second matrix operand
2865
     * @param int $matrix2Columns Column size of second matrix operand
2866
     */
2867
    private static function resizeMatricesExtend(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns)
2868
    {
2869
        if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
2870
            if ($matrix2Columns < $matrix1Columns) {
2871
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2872
                    $x = $matrix2[$i][$matrix2Columns - 1];
2873
                    for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
2874
                        $matrix2[$i][$j] = $x;
2875
                    }
2876
                }
2877
            }
2878
            if ($matrix2Rows < $matrix1Rows) {
2879
                $x = $matrix2[$matrix2Rows - 1];
2880
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2881
                    $matrix2[$i] = $x;
2882
                }
2883
            }
2884
        }
2885
2886
        if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
2887
            if ($matrix1Columns < $matrix2Columns) {
2888
                for ($i = 0; $i < $matrix1Rows; ++$i) {
2889
                    $x = $matrix1[$i][$matrix1Columns - 1];
2890
                    for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
2891
                        $matrix1[$i][$j] = $x;
2892
                    }
2893
                }
2894
            }
2895
            if ($matrix1Rows < $matrix2Rows) {
2896
                $x = $matrix1[$matrix1Rows - 1];
2897
                for ($i = 0; $i < $matrix2Rows; ++$i) {
2898
                    $matrix1[$i] = $x;
2899
                }
2900
            }
2901
        }
2902
    }
2903
2904
    /**
2905
     * Format details of an operand for display in the log (based on operand type).
2906
     *
2907
     * @param mixed $value First matrix operand
2908
     *
2909
     * @return mixed
2910
     */
2911 117
    private function showValue($value)
2912
    {
2913 117
        if ($this->debugLog->getWriteDebugLog()) {
2914
            $testArray = Functions::flattenArray($value);
2915
            if (count($testArray) == 1) {
2916
                $value = array_pop($testArray);
2917
            }
2918
2919
            if (is_array($value)) {
2920
                $returnMatrix = [];
2921
                $pad = $rpad = ', ';
2922
                foreach ($value as $row) {
2923
                    if (is_array($row)) {
2924
                        $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
2925
                        $rpad = '; ';
2926
                    } else {
2927
                        $returnMatrix[] = $this->showValue($row);
2928
                    }
2929
                }
2930
2931
                return '{ ' . implode($rpad, $returnMatrix) . ' }';
2932
            } elseif (is_string($value) && (trim($value, '"') == $value)) {
2933
                return '"' . $value . '"';
2934
            } elseif (is_bool($value)) {
2935
                return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
2936
            }
2937
        }
2938
2939 117
        return Functions::flattenSingleValue($value);
2940
    }
2941
2942
    /**
2943
     * Format type and details of an operand for display in the log (based on operand type).
2944
     *
2945
     * @param mixed $value First matrix operand
2946
     *
2947
     * @return null|string
2948
     */
2949 117
    private function showTypeDetails($value)
2950
    {
2951 117
        if ($this->debugLog->getWriteDebugLog()) {
2952
            $testArray = Functions::flattenArray($value);
2953
            if (count($testArray) == 1) {
2954
                $value = array_pop($testArray);
2955
            }
2956
2957
            if ($value === null) {
2958
                return 'a NULL value';
2959
            } elseif (is_float($value)) {
2960
                $typeString = 'a floating point number';
2961
            } elseif (is_int($value)) {
2962
                $typeString = 'an integer number';
2963
            } elseif (is_bool($value)) {
2964
                $typeString = 'a boolean';
2965
            } elseif (is_array($value)) {
2966
                $typeString = 'a matrix';
2967
            } else {
2968
                if ($value == '') {
2969
                    return 'an empty string';
2970
                } elseif ($value[0] == '#') {
2971
                    return 'a ' . $value . ' error';
2972
                }
2973
                $typeString = 'a string';
2974
            }
2975
2976
            return $typeString . ' with a value of ' . $this->showValue($value);
2977
        }
2978 117
    }
2979
2980
    /**
2981
     * @param string $formula
2982
     *
2983
     * @return string
2984
     */
2985 118
    private function convertMatrixReferences($formula)
2986
    {
2987 118
        static $matrixReplaceFrom = ['{', ';', '}'];
2988 118
        static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
2989
2990
        //    Convert any Excel matrix references to the MKMATRIX() function
2991 118
        if (strpos($formula, '{') !== false) {
2992
            //    If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
2993
            if (strpos($formula, '"') !== false) {
2994
                //    So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
2995
                //        the formula
2996
                $temp = explode('"', $formula);
2997
                //    Open and Closed counts used for trapping mismatched braces in the formula
2998
                $openCount = $closeCount = 0;
2999
                $i = false;
3000
                foreach ($temp as &$value) {
3001
                    //    Only count/replace in alternating array entries
3002
                    if ($i = !$i) {
3003
                        $openCount += substr_count($value, '{');
3004
                        $closeCount += substr_count($value, '}');
3005
                        $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
3006
                    }
3007
                }
3008
                unset($value);
3009
                //    Then rebuild the formula string
3010
                $formula = implode('"', $temp);
3011
            } else {
3012
                //    If there's no quoted strings, then we do a simple count/replace
3013
                $openCount = substr_count($formula, '{');
3014
                $closeCount = substr_count($formula, '}');
3015
                $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
3016
            }
3017
            //    Trap for mismatched braces and trigger an appropriate error
3018
            if ($openCount < $closeCount) {
3019
                if ($openCount > 0) {
3020
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
3021
                }
3022
3023
                return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
3024
            } elseif ($openCount > $closeCount) {
3025
                if ($closeCount > 0) {
3026
                    return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
3027
                }
3028
3029
                return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
3030
            }
3031
        }
3032
3033 118
        return $formula;
3034
    }
3035
3036
    private static function mkMatrix(...$args)
3037
    {
3038
        return $args;
3039
    }
3040
3041
    //    Binary Operators
3042
    //    These operators always work on two values
3043
    //    Array key is the operator, the value indicates whether this is a left or right associative operator
3044
    private static $operatorAssociativity = [
3045
        '^' => 0, //    Exponentiation
3046
        '*' => 0, '/' => 0, //    Multiplication and Division
3047
        '+' => 0, '-' => 0, //    Addition and Subtraction
3048
        '&' => 0, //    Concatenation
3049
        '|' => 0, ':' => 0, //    Intersect and Range
3050
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3051
    ];
3052
3053
    //    Comparison (Boolean) Operators
3054
    //    These operators work on two values, but always return a boolean result
3055
    private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
3056
3057
    //    Operator Precedence
3058
    //    This list includes all valid operators, whether binary (including boolean) or unary (such as %)
3059
    //    Array key is the operator, the value is its precedence
3060
    private static $operatorPrecedence = [
3061
        ':' => 8, //    Range
3062
        '|' => 7, //    Intersect
3063
        '~' => 6, //    Negation
3064
        '%' => 5, //    Percentage
3065
        '^' => 4, //    Exponentiation
3066
        '*' => 3, '/' => 3, //    Multiplication and Division
3067
        '+' => 2, '-' => 2, //    Addition and Subtraction
3068
        '&' => 1, //    Concatenation
3069
        '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, //    Comparison
3070
    ];
3071
3072
    // Convert infix to postfix notation
3073
3074
    /**
3075
     * @param string $formula
3076
     * @param null|\PhpOffice\PhpSpreadsheet\Cell\Cell $pCell
3077
     *
3078
     * @return bool
3079
     */
3080 118
    private function _parseFormula($formula, Cell $pCell = null)
3081
    {
3082 118
        if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
3083
            return false;
3084
        }
3085
3086
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
3087
        //        so we store the parent worksheet so that we can re-attach it when necessary
3088 118
        $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null;
3089
3090 118
        $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION .
3091 118
                                '|' . self::CALCULATION_REGEXP_CELLREF .
3092 118
                                '|' . self::CALCULATION_REGEXP_NUMBER .
3093 118
                                '|' . self::CALCULATION_REGEXP_STRING .
3094 118
                                '|' . self::CALCULATION_REGEXP_OPENBRACE .
3095 118
                                '|' . self::CALCULATION_REGEXP_NAMEDRANGE .
3096 118
                                '|' . self::CALCULATION_REGEXP_ERROR .
3097 118
                                ')/si';
3098
3099
        //    Start with initialisation
3100 118
        $index = 0;
3101 118
        $stack = new Stack();
3102 118
        $output = [];
3103 118
        $expectingOperator = false; //    We use this test in syntax-checking the expression to determine when a
3104
                                                    //        - is a negation or + is a positive operator rather than an operation
3105 118
        $expectingOperand = false; //    We use this test in syntax-checking the expression to determine whether an operand
3106
                                                    //        should be null in a function call
3107
        //    The guts of the lexical parser
3108
        //    Loop through the formula extracting each operator and operand in turn
3109 118
        while (true) {
3110 118
            $opCharacter = $formula[$index]; //    Get the first character of the value at the current index position
3111 118
            if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
3112 34
                $opCharacter .= $formula[++$index];
3113
            }
3114
3115
            //    Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand
3116 118
            $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
3117
3118 118
            if ($opCharacter == '-' && !$expectingOperator) {                //    Is it a negation instead of a minus?
3119 2
                $stack->push('Unary Operator', '~'); //    Put a negation on the stack
3120 2
                ++$index; //        and drop the negation symbol
3121 118
            } elseif ($opCharacter == '%' && $expectingOperator) {
3122
                $stack->push('Unary Operator', '%'); //    Put a percentage on the stack
3123
                ++$index;
3124 118
            } elseif ($opCharacter == '+' && !$expectingOperator) {            //    Positive (unary plus rather than binary operator plus) can be discarded?
3125
                ++$index; //    Drop the redundant plus symbol
3126 118
            } elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) {    //    We have to explicitly deny a tilde or pipe, because they are legal
3127
                return $this->raiseFormulaError("Formula Error: Illegal character '~'"); //        on the stack but not in the input expression
3128 118
            } elseif ((isset(self::$operators[$opCharacter]) or $isOperandOrFunction) && $expectingOperator) {    //    Are we putting an operator on the stack?
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

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

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

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

Let’s take a look at a few examples:

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

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


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

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

Logical Operators are used for Control-Flow

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

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

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

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

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

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

Loading history...
3129 111
                while ($stack->count() > 0 &&
3130 111
                    ($o2 = $stack->last()) &&
3131 111
                    isset(self::$operators[$o2['value']]) &&
3132 111
                    @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3133 2
                    $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3134
                }
3135 111
                $stack->push('Binary Operator', $opCharacter); //    Finally put our current operator onto the stack
3136 111
                ++$index;
3137 111
                $expectingOperator = false;
3138 118
            } elseif ($opCharacter == ')' && $expectingOperator) {            //    Are we expecting to close a parenthesis?
3139 46
                $expectingOperand = false;
3140 46
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3141 39
                    if ($o2 === null) {
3142
                        return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"');
3143
                    }
3144 39
                    $output[] = $o2;
3145
                }
3146 46
                $d = $stack->last(2);
3147 46
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {    //    Did this parenthesis just close a function?
3148 46
                    $functionName = $matches[1]; //    Get the function name
3149 46
                    $d = $stack->pop();
3150 46
                    $argumentCount = $d['value']; //    See how many arguments there were (argument count is the next value stored on the stack)
3151 46
                    $output[] = $d; //    Dump the argument count on the output
3152 46
                    $output[] = $stack->pop(); //    Pop the function and push onto the output
3153 46
                    if (isset(self::$controlFunctions[$functionName])) {
3154
                        $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
3155
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
3156 46
                    } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3157 46
                        $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
3158 46
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3159
                    } else {    // did we somehow push a non-function on the stack? this should never happen
3160
                        return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack');
3161
                    }
3162
                    //    Check the argument count
3163 46
                    $argumentCountError = false;
3164 46
                    if (is_numeric($expectedArgumentCount)) {
3165 25
                        if ($expectedArgumentCount < 0) {
3166
                            if ($argumentCount > abs($expectedArgumentCount)) {
3167
                                $argumentCountError = true;
3168
                                $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
3169
                            }
3170
                        } else {
3171 25
                            if ($argumentCount != $expectedArgumentCount) {
3172
                                $argumentCountError = true;
3173 25
                                $expectedArgumentCountString = $expectedArgumentCount;
3174
                            }
3175
                        }
3176 40
                    } elseif ($expectedArgumentCount != '*') {
3177 40
                        $isOperandOrFunction = preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch);
3178 40
                        switch ($argMatch[2]) {
3179 40
                            case '+':
3180 38
                                if ($argumentCount < $argMatch[1]) {
3181
                                    $argumentCountError = true;
3182
                                    $expectedArgumentCountString = $argMatch[1] . ' or more ';
3183
                                }
3184
3185 38
                                break;
3186 16
                            case '-':
3187 13
                                if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
3188
                                    $argumentCountError = true;
3189
                                    $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
3190
                                }
3191
3192 13
                                break;
3193 3
                            case ',':
3194 3
                                if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
3195
                                    $argumentCountError = true;
3196
                                    $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
3197
                                }
3198
3199 3
                                break;
3200
                        }
3201
                    }
3202 46
                    if ($argumentCountError) {
3203
                        return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected');
3204
                    }
3205
                }
3206 46
                ++$index;
3207 118
            } elseif ($opCharacter == ',') {            //    Is this the separator for function arguments?
3208 30
                while (($o2 = $stack->pop()) && $o2['value'] != '(') {        //    Pop off the stack back to the last (
3209 25
                    if ($o2 === null) {
3210
                        return $this->raiseFormulaError('Formula Error: Unexpected ,');
3211
                    }
3212 25
                    $output[] = $o2; // pop the argument expression stuff and push onto the output
3213
                }
3214
                //    If we've a comma when we're expecting an operand, then what we actually have is a null operand;
3215
                //        so push a null onto the stack
3216 30
                if (($expectingOperand) || (!$expectingOperator)) {
3217
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3218
                }
3219
                // make sure there was a function
3220 30
                $d = $stack->last(2);
3221 30
                if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) {
3222
                    return $this->raiseFormulaError('Formula Error: Unexpected ,');
3223
                }
3224 30
                $d = $stack->pop();
3225 30
                $stack->push($d['type'], ++$d['value'], $d['reference']); // increment the argument count
3226 30
                $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again
3227 30
                $expectingOperator = false;
3228 30
                $expectingOperand = true;
3229 30
                ++$index;
3230 118
            } elseif ($opCharacter == '(' && !$expectingOperator) {
3231 3
                $stack->push('Brace', '(');
3232 3
                ++$index;
3233 118
            } elseif ($isOperandOrFunction && !$expectingOperator) {    // do we now have a function/variable/number?
3234 118
                $expectingOperator = true;
3235 118
                $expectingOperand = false;
3236 118
                $val = $match[1];
3237 118
                $length = strlen($val);
3238
3239 118
                if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
3240 46
                    $val = preg_replace('/\s/u', '', $val);
3241 46
                    if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) {    // it's a function
3242 46
                        $stack->push('Function', strtoupper($val));
3243 46
                        $ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
3244 46
                        if ($ax) {
3245 9
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 0);
3246 9
                            $expectingOperator = true;
3247
                        } else {
3248 46
                            $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 1);
3249 46
                            $expectingOperator = false;
3250
                        }
3251 46
                        $stack->push('Brace', '(');
3252
                    } else {    // it's a var w/ implicit multiplication
3253 46
                        $output[] = ['type' => 'Value', 'value' => $matches[1], 'reference' => null];
3254
                    }
3255 118
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
3256
                    //    Watch for this case-change when modifying to allow cell references in different worksheets...
3257
                    //    Should only be applied to the actual cell column, not the worksheet name
3258
3259
                    //    If the last entry on the stack was a : operator, then we have a cell range reference
3260 60
                    $testPrevOp = $stack->last(1);
3261 60
                    if ($testPrevOp['value'] == ':') {
3262
                        //    If we have a worksheet reference, then we're playing with a 3D reference
3263 52
                        if ($matches[2] == '') {
3264
                            //    Otherwise, we 'inherit' the worksheet reference from the start cell reference
3265
                            //    The start of the cell range reference should be the last entry in $output
3266 52
                            $startCellRef = $output[count($output) - 1]['value'];
3267 52
                            preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $startCellRef, $startMatches);
3268 52
                            if ($startMatches[2] > '') {
3269 52
                                $val = $startMatches[2] . '!' . $val;
3270
                            }
3271
                        } else {
3272
                            return $this->raiseFormulaError('3D Range references are not yet supported');
3273
                        }
3274
                    }
3275
3276 60
                    $output[] = ['type' => 'Cell Reference', 'value' => $val, 'reference' => $val];
3277
                } else {    // it's a variable, constant, string, number or boolean
3278
                    //    If the last entry on the stack was a : operator, then we may have a row or column range reference
3279 95
                    $testPrevOp = $stack->last(1);
3280 95
                    if ($testPrevOp['value'] == ':') {
3281 1
                        $startRowColRef = $output[count($output) - 1]['value'];
3282 1
                        $rangeWS1 = '';
3283 1
                        if (strpos('!', $startRowColRef) !== false) {
3284
                            list($rangeWS1, $startRowColRef) = explode('!', $startRowColRef);
3285
                        }
3286 1
                        if ($rangeWS1 != '') {
3287
                            $rangeWS1 .= '!';
3288
                        }
3289 1
                        $rangeWS2 = $rangeWS1;
3290 1
                        if (strpos('!', $val) !== false) {
3291
                            list($rangeWS2, $val) = explode('!', $val);
3292
                        }
3293 1
                        if ($rangeWS2 != '') {
3294
                            $rangeWS2 .= '!';
3295
                        }
3296 1
                        if ((is_int($startRowColRef)) && (ctype_digit($val)) &&
3297 1
                            ($startRowColRef <= 1048576) && ($val <= 1048576)) {
3298
                            //    Row range
3299
                            $endRowColRef = ($pCellParent !== null) ? $pCellParent->getHighestColumn() : 'XFD'; //    Max 16,384 columns for Excel2007
3300
                            $output[count($output) - 1]['value'] = $rangeWS1 . 'A' . $startRowColRef;
3301
                            $val = $rangeWS2 . $endRowColRef . $val;
3302 1
                        } elseif ((ctype_alpha($startRowColRef)) && (ctype_alpha($val)) &&
3303 1
                            (strlen($startRowColRef) <= 3) && (strlen($val) <= 3)) {
3304
                            //    Column range
3305
                            $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...
3306
                            $output[count($output) - 1]['value'] = $rangeWS1 . strtoupper($startRowColRef) . '1';
3307
                            $val = $rangeWS2 . $val . $endRowColRef;
3308
                        }
3309
                    }
3310
3311 95
                    $localeConstant = false;
3312 95
                    if ($opCharacter == '"') {
3313
                        //    UnEscape any quotes within the string
3314 59
                        $val = self::wrapResult(str_replace('""', '"', self::unwrapResult($val)));
3315 62
                    } elseif (is_numeric($val)) {
3316 60
                        if ((strpos($val, '.') !== false) || (stripos($val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
3317 21
                            $val = (float) $val;
3318
                        } else {
3319 60
                            $val = (int) $val;
3320
                        }
3321 6
                    } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
3322 1
                        $excelConstant = trim(strtoupper($val));
3323 1
                        $val = self::$excelConstants[$excelConstant];
3324 5
                    } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
3325
                        $val = self::$excelConstants[$localeConstant];
3326
                    }
3327 95
                    $details = ['type' => 'Value', 'value' => $val, 'reference' => null];
3328 95
                    if ($localeConstant) {
3329
                        $details['localeValue'] = $localeConstant;
3330
                    }
3331 95
                    $output[] = $details;
3332
                }
3333 118
                $index += $length;
3334
            } elseif ($opCharacter == '$') {    // absolute row or column range
3335
                ++$index;
3336
            } elseif ($opCharacter == ')') {    // miscellaneous error checking
3337
                if ($expectingOperand) {
3338
                    $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null];
3339
                    $expectingOperand = false;
3340
                    $expectingOperator = true;
3341
                } else {
3342
                    return $this->raiseFormulaError("Formula Error: Unexpected ')'");
3343
                }
3344
            } elseif (isset(self::$operators[$opCharacter]) && !$expectingOperator) {
3345
                return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
3346
            } else {    // I don't even want to know what you did to get here
3347
                return $this->raiseFormulaError('Formula Error: An unexpected error occured');
3348
            }
3349
            //    Test for end of formula string
3350 118
            if ($index == strlen($formula)) {
3351
                //    Did we end with an operator?.
3352
                //    Only valid for the % unary operator
3353 118
                if ((isset(self::$operators[$opCharacter])) && ($opCharacter != '%')) {
3354
                    return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
3355
                }
3356
3357 118
                break;
3358
            }
3359
            //    Ignore white space
3360 117
            while (($formula[$index] == "\n") || ($formula[$index] == "\r")) {
3361
                ++$index;
3362
            }
3363 117
            if ($formula[$index] == ' ') {
3364 55
                while ($formula[$index] == ' ') {
3365 55
                    ++$index;
3366
                }
3367
                //    If we're expecting an operator, but only have a space between the previous and next operands (and both are
3368
                //        Cell References) then we have an INTERSECTION operator
3369 55
                if (($expectingOperator) && (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
3370 55
                    ($output[count($output) - 1]['type'] == 'Cell Reference')) {
3371
                    while ($stack->count() > 0 &&
3372
                        ($o2 = $stack->last()) &&
3373
                        isset(self::$operators[$o2['value']]) &&
3374
                        @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
3375
                        $output[] = $stack->pop(); //    Swap operands and higher precedence operators from the stack to the output
3376
                    }
3377
                    $stack->push('Binary Operator', '|'); //    Put an Intersect Operator on the stack
3378
                    $expectingOperator = false;
3379
                }
3380
            }
3381
        }
3382
3383 118
        while (($op = $stack->pop()) !== null) {    // pop everything off the stack and push onto output
3384 98
            if ((is_array($op) && $op['value'] == '(') || ($op === '(')) {
3385
                return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
3386
            }
3387 98
            $output[] = $op;
3388
        }
3389
3390 118
        return $output;
3391
    }
3392
3393 111
    private static function dataTestReference(&$operandData)
3394
    {
3395 111
        $operand = $operandData['value'];
3396 111
        if (($operandData['reference'] === null) && (is_array($operand))) {
3397 1
            $rKeys = array_keys($operand);
3398 1
            $rowKey = array_shift($rKeys);
3399 1
            $cKeys = array_keys(array_keys($operand[$rowKey]));
3400 1
            $colKey = array_shift($cKeys);
3401 1
            if (ctype_upper($colKey)) {
3402
                $operandData['reference'] = $colKey . $rowKey;
3403
            }
3404
        }
3405
3406 111
        return $operand;
3407
    }
3408
3409
    // evaluate postfix notation
3410
3411
    /**
3412
     * @param mixed $tokens
3413
     * @param null|string $cellID
3414
     * @param null|Cell $pCell
3415
     *
3416
     * @return bool
3417
     */
3418 118
    private function processTokenStack($tokens, $cellID = null, Cell $pCell = null)
3419
    {
3420 118
        if ($tokens == false) {
3421
            return false;
3422
        }
3423
3424
        //    If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
3425
        //        so we store the parent cell collection so that we can re-attach it when necessary
3426 118
        $pCellWorksheet = ($pCell !== null) ? $pCell->getWorksheet() : null;
3427 118
        $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
3428 118
        $stack = new Stack();
3429
3430
        //    Loop through each token in turn
3431 118
        foreach ($tokens as $tokenData) {
3432 118
            $token = $tokenData['value'];
3433
            // 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
3434 118
            if (isset(self::$binaryOperators[$token])) {
3435
                //    We must have two operands, error if we don't
3436 111
                if (($operand2Data = $stack->pop()) === null) {
3437
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3438
                }
3439 111
                if (($operand1Data = $stack->pop()) === null) {
3440
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3441
                }
3442
3443 111
                $operand1 = self::dataTestReference($operand1Data);
3444 111
                $operand2 = self::dataTestReference($operand2Data);
3445
3446
                //    Log what we're doing
3447 111
                if ($token == ':') {
3448 52
                    $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference']));
3449
                } else {
3450 94
                    $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2));
3451
                }
3452
3453
                //    Process the operation in the appropriate manner
3454
                switch ($token) {
3455
                    //    Comparison (Boolean) Operators
3456 111
                    case '>':            //    Greater than
3457 97
                    case '<':            //    Less than
3458 90
                    case '>=':            //    Greater than or Equal to
3459 82
                    case '<=':            //    Less than or Equal to
3460 75
                    case '=':            //    Equality
3461 66
                    case '<>':            //    Inequality
3462 74
                        $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
3463
3464 74
                        break;
3465
                    //    Binary Operators
3466 59
                    case ':':            //    Range
3467 52
                        $sheet1 = $sheet2 = '';
3468 52
                        if (strpos($operand1Data['reference'], '!') !== false) {
3469 52
                            list($sheet1, $operand1Data['reference']) = explode('!', $operand1Data['reference']);
3470
                        } else {
3471 1
                            $sheet1 = ($pCellParent !== null) ? $pCellWorksheet->getTitle() : '';
3472
                        }
3473 52
                        if (strpos($operand2Data['reference'], '!') !== false) {
3474 52
                            list($sheet2, $operand2Data['reference']) = explode('!', $operand2Data['reference']);
3475
                        } else {
3476 10
                            $sheet2 = $sheet1;
3477
                        }
3478 52
                        if ($sheet1 == $sheet2) {
3479 52
                            if ($operand1Data['reference'] === null) {
3480
                                if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
3481
                                    $operand1Data['reference'] = $pCell->getColumn() . $operand1Data['value'];
3482
                                } elseif (trim($operand1Data['reference']) == '') {
3483
                                    $operand1Data['reference'] = $pCell->getCoordinate();
3484
                                } else {
3485
                                    $operand1Data['reference'] = $operand1Data['value'] . $pCell->getRow();
3486
                                }
3487
                            }
3488 52
                            if ($operand2Data['reference'] === null) {
3489
                                if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
3490
                                    $operand2Data['reference'] = $pCell->getColumn() . $operand2Data['value'];
3491
                                } elseif (trim($operand2Data['reference']) == '') {
3492
                                    $operand2Data['reference'] = $pCell->getCoordinate();
3493
                                } else {
3494
                                    $operand2Data['reference'] = $operand2Data['value'] . $pCell->getRow();
3495
                                }
3496
                            }
3497
3498 52
                            $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
3499 52
                            $oCol = $oRow = [];
3500 52
                            foreach ($oData as $oDatum) {
3501 52
                                $oCR = Coordinate::coordinateFromString($oDatum);
3502 52
                                $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
3503 52
                                $oRow[] = $oCR[1];
3504
                            }
3505 52
                            $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
3506 52
                            if ($pCellParent !== null) {
3507 52
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
3508
                            } else {
3509
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3510
                            }
3511 52
                            $stack->push('Cell Reference', $cellValue, $cellRef);
3512
                        } else {
3513
                            $stack->push('Error', Functions::REF(), null);
3514
                        }
3515
3516 52
                        break;
3517 32
                    case '+':            //    Addition
3518 23
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
3519
3520 23
                        break;
3521 28
                    case '-':            //    Subtraction
3522 7
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
3523
3524 7
                        break;
3525 26
                    case '*':            //    Multiplication
3526 19
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
3527
3528 19
                        break;
3529 11
                    case '/':            //    Division
3530 8
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
3531
3532 8
                        break;
3533 6
                    case '^':            //    Exponential
3534
                        $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
3535
3536
                        break;
3537 6
                    case '&':            //    Concatenation
3538
                        //    If either of the operands is a matrix, we need to treat them both as matrices
3539
                        //        (converting the other operand to a matrix if need be); then perform the required
3540
                        //        matrix operation
3541 6
                        if (is_bool($operand1)) {
3542
                            $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3543
                        }
3544 6
                        if (is_bool($operand2)) {
3545
                            $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3546
                        }
3547 6
                        if ((is_array($operand1)) || (is_array($operand2))) {
3548
                            //    Ensure that both operands are arrays/matrices
3549 5
                            self::checkMatrixOperands($operand1, $operand2, 2);
3550
3551
                            try {
3552
                                //    Convert operand 1 from a PHP array to a matrix
3553 5
                                $matrix = new Shared\JAMA\Matrix($operand1);
3554
                                //    Perform the required operation against the operand 1 matrix, passing in operand 2
3555 5
                                $matrixResult = $matrix->concat($operand2);
3556 5
                                $result = $matrixResult->getArray();
3557
                            } catch (\Exception $ex) {
3558
                                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3559 5
                                $result = '#VALUE!';
3560
                            }
3561
                        } else {
3562 1
                            $result = '"' . str_replace('""', '"', self::unwrapResult($operand1) . self::unwrapResult($operand2)) . '"';
3563
                        }
3564 6
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3565 6
                        $stack->push('Value', $result);
3566
3567 6
                        break;
3568
                    case '|':            //    Intersect
3569
                        $rowIntersect = array_intersect_key($operand1, $operand2);
3570
                        $cellIntersect = $oCol = $oRow = [];
3571
                        foreach (array_keys($rowIntersect) as $row) {
3572
                            $oRow[] = $row;
3573
                            foreach ($rowIntersect[$row] as $col => $data) {
3574
                                $oCol[] = Coordinate::columnIndexFromString($col) - 1;
3575
                                $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
3576
                            }
3577
                        }
3578
                        $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
3579
                        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect));
3580
                        $stack->push('Value', $cellIntersect, $cellRef);
3581
3582 111
                        break;
3583
                }
3584
3585
                // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
3586 118
            } elseif (($token === '~') || ($token === '%')) {
3587 2
                if (($arg = $stack->pop()) === null) {
3588
                    return $this->raiseFormulaError('Internal error - Operand value missing from stack');
3589
                }
3590 2
                $arg = $arg['value'];
3591 2
                if ($token === '~') {
3592 2
                    $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg));
3593 2
                    $multiplier = -1;
3594
                } else {
3595
                    $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg));
3596
                    $multiplier = 0.01;
3597
                }
3598 2
                if (is_array($arg)) {
3599
                    self::checkMatrixOperands($arg, $multiplier, 2);
3600
3601
                    try {
3602
                        $matrix1 = new Shared\JAMA\Matrix($arg);
3603
                        $matrixResult = $matrix1->arrayTimesEquals($multiplier);
3604
                        $result = $matrixResult->getArray();
3605
                    } catch (\Exception $ex) {
3606
                        $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
3607
                        $result = '#VALUE!';
3608
                    }
3609
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3610
                    $stack->push('Value', $result);
3611
                } else {
3612 2
                    $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack);
3613
                }
3614 118
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token, $matches)) {
3615 60
                $cellRef = null;
3616 60
                if (isset($matches[8])) {
3617
                    if ($pCell === null) {
3618
                        //                        We can't access the range, so return a REF error
3619
                        $cellValue = Functions::REF();
3620
                    } else {
3621
                        $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
3622
                        if ($matches[2] > '') {
3623
                            $matches[2] = trim($matches[2], "\"'");
3624
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
3625
                                //    It's a Reference to an external spreadsheet (not currently supported)
3626
                                return $this->raiseFormulaError('Unable to access External Workbook');
3627
                            }
3628
                            $matches[2] = trim($matches[2], "\"'");
3629
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]);
3630
                            if ($pCellParent !== null) {
3631
                                $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3632
                            } else {
3633
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3634
                            }
3635
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3636
                        } else {
3637
                            $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet');
3638
                            if ($pCellParent !== null) {
3639
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
3640
                            } else {
3641
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3642
                            }
3643
                            $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
3644
                        }
3645
                    }
3646
                } else {
3647 60
                    if ($pCell === null) {
3648
                        //                        We can't access the cell, so return a REF error
3649
                        $cellValue = Functions::REF();
3650
                    } else {
3651 60
                        $cellRef = $matches[6] . $matches[7];
3652 60
                        if ($matches[2] > '') {
3653 14
                            $matches[2] = trim($matches[2], "\"'");
3654 14
                            if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
3655
                                //    It's a Reference to an external spreadsheet (not currently supported)
3656
                                return $this->raiseFormulaError('Unable to access External Workbook');
3657
                            }
3658 14
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]);
3659 14
                            if ($pCellParent !== null) {
3660 14
                                $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
3661 14
                                if ($cellSheet && $cellSheet->cellExists($cellRef)) {
3662 14
                                    $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
3663 14
                                    $pCell->attach($pCellParent);
3664
                                } else {
3665 14
                                    $cellValue = null;
3666
                                }
3667
                            } else {
3668
                                return $this->raiseFormulaError('Unable to access Cell Reference');
3669
                            }
3670 14
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue));
3671
                        } else {
3672 48
                            $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet');
3673 48
                            if ($pCellParent->has($cellRef)) {
3674 48
                                $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
3675 48
                                $pCell->attach($pCellParent);
3676
                            } else {
3677 2
                                $cellValue = null;
3678
                            }
3679 48
                            $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' is ', $this->showTypeDetails($cellValue));
3680
                        }
3681
                    }
3682
                }
3683 60
                $stack->push('Value', $cellValue, $cellRef);
3684
3685
            // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
3686 104
            } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) {
3687 46
                $functionName = $matches[1];
3688 46
                $argCount = $stack->pop();
3689 46
                $argCount = $argCount['value'];
3690 46
                if ($functionName != 'MKMATRIX') {
3691 46
                    $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's'));
3692
                }
3693 46
                if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) {    // function
3694 46
                    if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
3695 46
                        $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
3696 46
                        $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
3697 46
                        $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
3698
                    } elseif (isset(self::$controlFunctions[$functionName])) {
3699
                        $functionCall = self::$controlFunctions[$functionName]['functionCall'];
3700
                        $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
3701
                        $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
3702
                    }
3703
                    // get the arguments for this function
3704 46
                    $args = $argArrayVals = [];
3705 46
                    for ($i = 0; $i < $argCount; ++$i) {
3706 46
                        $arg = $stack->pop();
3707 46
                        $a = $argCount - $i - 1;
3708 46
                        if (($passByReference) &&
3709 46
                            (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) &&
3710 46
                            (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) {
3711
                            if ($arg['reference'] === null) {
3712
                                $args[] = $cellID;
3713
                                if ($functionName != 'MKMATRIX') {
3714
                                    $argArrayVals[] = $this->showValue($cellID);
3715
                                }
3716
                            } else {
3717
                                $args[] = $arg['reference'];
3718
                                if ($functionName != 'MKMATRIX') {
3719
                                    $argArrayVals[] = $this->showValue($arg['reference']);
3720
                                }
3721
                            }
3722
                        } else {
3723 46
                            $args[] = self::unwrapResult($arg['value']);
3724 46
                            if ($functionName != 'MKMATRIX') {
3725 46
                                $argArrayVals[] = $this->showValue($arg['value']);
3726
                            }
3727
                        }
3728
                    }
3729
                    //    Reverse the order of the arguments
3730 46
                    krsort($args);
3731
3732 46
                    if (($passByReference) && ($argCount == 0)) {
3733
                        $args[] = $cellID;
3734
                        $argArrayVals[] = $this->showValue($cellID);
3735
                    }
3736
3737 46
                    if ($functionName != 'MKMATRIX') {
3738 46
                        if ($this->debugLog->getWriteDebugLog()) {
3739
                            krsort($argArrayVals);
3740
                            $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )');
3741
                        }
3742
                    }
3743
3744
                    //    Process the argument with the appropriate function call
3745 46
                    if ($passCellReference) {
3746 1
                        $args[] = $pCell;
3747
                    }
3748
3749 46
                    if (!is_array($functionCall)) {
3750 1
                        foreach ($args as &$arg) {
3751
                            $arg = Functions::flattenSingleValue($arg);
3752
                        }
3753 1
                        unset($arg);
3754
                    }
3755 46
                    $result = call_user_func_array($functionCall, $args);
3756
3757 46
                    if ($functionName != 'MKMATRIX') {
3758 46
                        $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
3759
                    }
3760 46
                    $stack->push('Value', self::wrapResult($result));
3761
                }
3762
            } else {
3763
                // if the token is a number, boolean, string or an Excel error, push it onto the stack
3764 104
                if (isset(self::$excelConstants[strtoupper($token)])) {
3765
                    $excelConstant = strtoupper($token);
3766
                    $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
3767
                    $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant]));
3768 104
                } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == '"') || ($token[0] == '#')) {
3769 104
                    $stack->push('Value', $token);
3770
                // if the token is a named range, push the named range name onto the stack
3771 5
                } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) {
3772 5
                    $namedRange = $matches[6];
3773 5
                    $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
3774
3775 5
                    if (substr($namedRange, 0, 6) === '_xlfn.') {
3776
                        return $this->raiseFormulaError("undefined named range / function '$token'");
3777
                    }
3778
3779 5
                    $cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
3780 5
                    $pCell->attach($pCellParent);
3781 5
                    $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
3782 5
                    $stack->push('Named Range', $cellValue, $namedRange);
3783
                } else {
3784 118
                    return $this->raiseFormulaError("undefined variable '$token'");
3785
                }
3786
            }
3787
        }
3788
        // when we're out of tokens, the stack should have a single element, the final result
3789 118
        if ($stack->count() != 1) {
3790
            return $this->raiseFormulaError('internal error');
3791
        }
3792 118
        $output = $stack->pop();
3793 118
        $output = $output['value'];
3794
3795 118
        return $output;
3796
    }
3797
3798 31
    private function validateBinaryOperand(&$operand, &$stack)
3799
    {
3800 31
        if (is_array($operand)) {
3801 22
            if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
3802
                do {
3803 22
                    $operand = array_pop($operand);
3804 22
                } while (is_array($operand));
3805
            }
3806
        }
3807
        //    Numbers, matrices and booleans can pass straight through, as they're already valid
3808 31
        if (is_string($operand)) {
3809
            //    We only need special validations for the operand if it is a string
3810
            //    Start by stripping off the quotation marks we use to identify true excel string values internally
3811 2
            if ($operand > '' && $operand[0] == '"') {
3812
                $operand = self::unwrapResult($operand);
3813
            }
3814
            //    If the string is a numeric value, we treat it as a numeric, so no further testing
3815 2
            if (!is_numeric($operand)) {
3816
                //    If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
3817 2
                if ($operand > '' && $operand[0] == '#') {
3818
                    $stack->push('Value', $operand);
3819
                    $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand));
3820
3821
                    return false;
3822 2
                } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
3823
                    //    If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
3824 2
                    $stack->push('Value', '#VALUE!');
3825 2
                    $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!'));
3826
3827 2
                    return false;
3828
                }
3829
            }
3830
        }
3831
3832
        //    return a true if the value of the operand is one that we can use in normal binary operations
3833 30
        return true;
3834
    }
3835
3836
    /**
3837
     * @param null|string $cellID
3838
     * @param mixed $operand1
3839
     * @param mixed $operand2
3840
     * @param string $operation
3841
     * @param Stack $stack
3842
     * @param bool $recursingArrays
3843
     *
3844
     * @return bool
3845
     */
3846 74
    private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
3847
    {
3848
        //    If we're dealing with matrix operations, we want a matrix result
3849 74
        if ((is_array($operand1)) || (is_array($operand2))) {
3850 12
            $result = [];
3851 12
            if ((is_array($operand1)) && (!is_array($operand2))) {
3852 12
                foreach ($operand1 as $x => $operandData) {
3853 12
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2));
3854 12
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack);
3855 12
                    $r = $stack->pop();
3856 12
                    $result[$x] = $r['value'];
3857
                }
3858
            } elseif ((!is_array($operand1)) && (is_array($operand2))) {
3859
                foreach ($operand2 as $x => $operandData) {
3860
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData));
3861
                    $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack);
3862
                    $r = $stack->pop();
3863
                    $result[$x] = $r['value'];
3864
                }
3865
            } else {
3866
                if (!$recursingArrays) {
3867
                    self::checkMatrixOperands($operand1, $operand2, 2);
3868
                }
3869
                foreach ($operand1 as $x => $operandData) {
3870
                    $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x]));
3871
                    $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true);
3872
                    $r = $stack->pop();
3873
                    $result[$x] = $r['value'];
3874
                }
3875
            }
3876
            //    Log the result details
3877 12
            $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result));
3878
            //    And push the result onto the stack
3879 12
            $stack->push('Array', $result);
3880
3881 12
            return true;
3882
        }
3883
3884
        //    Simple validate the two operands if they are string values
3885 74
        if (is_string($operand1) && $operand1 > '' && $operand1[0] == '"') {
3886 43
            $operand1 = self::unwrapResult($operand1);
3887
        }
3888 74
        if (is_string($operand2) && $operand2 > '' && $operand2[0] == '"') {
3889 55
            $operand2 = self::unwrapResult($operand2);
3890
        }
3891
3892
        // Use case insensitive comparaison if not OpenOffice mode
3893 74
        if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
3894 74
            if (is_string($operand1)) {
3895 43
                $operand1 = strtoupper($operand1);
3896
            }
3897 74
            if (is_string($operand2)) {
3898 55
                $operand2 = strtoupper($operand2);
3899
            }
3900
        }
3901
3902 74
        $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE;
3903
3904
        //    execute the necessary operation
3905
        switch ($operation) {
3906
            //    Greater than
3907 74
            case '>':
3908 18
                if ($useLowercaseFirstComparison) {
3909 9
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0;
3910
                } else {
3911 18
                    $result = ($operand1 > $operand2);
3912
                }
3913
3914 18
                break;
3915
            //    Less than
3916 60
            case '<':
3917 8
                if ($useLowercaseFirstComparison) {
3918 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0;
3919
                } else {
3920 8
                    $result = ($operand1 < $operand2);
3921
                }
3922
3923 8
                break;
3924
            //    Equality
3925 53
            case '=':
3926 19
                if (is_numeric($operand1) && is_numeric($operand2)) {
3927 3
                    $result = (abs($operand1 - $operand2) < $this->delta);
3928
                } else {
3929 16
                    $result = strcmp($operand1, $operand2) == 0;
3930
                }
3931
3932 19
                break;
3933
            //    Greater than or equal
3934 34
            case '>=':
3935 8
                if (is_numeric($operand1) && is_numeric($operand2)) {
3936 4
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2));
3937 4
                } elseif ($useLowercaseFirstComparison) {
3938 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0;
3939
                } else {
3940 4
                    $result = strcmp($operand1, $operand2) >= 0;
3941
                }
3942
3943 8
                break;
3944
            //    Less than or equal
3945 26
            case '<=':
3946 7
                if (is_numeric($operand1) && is_numeric($operand2)) {
3947 3
                    $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2));
3948 4
                } elseif ($useLowercaseFirstComparison) {
3949 4
                    $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0;
3950
                } else {
3951 4
                    $result = strcmp($operand1, $operand2) <= 0;
3952
                }
3953
3954 7
                break;
3955
            //    Inequality
3956 19
            case '<>':
3957 19
                if (is_numeric($operand1) && is_numeric($operand2)) {
3958 3
                    $result = (abs($operand1 - $operand2) > 1E-14);
3959
                } else {
3960 16
                    $result = strcmp($operand1, $operand2) != 0;
3961
                }
3962
3963 19
                break;
3964
        }
3965
3966
        //    Log the result details
3967 74
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
3968
        //    And push the result onto the stack
3969 74
        $stack->push('Value', $result);
3970
3971 74
        return true;
3972
    }
3973
3974
    /**
3975
     * Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
3976
     *
3977
     * @param string $str1 First string value for the comparison
3978
     * @param string $str2 Second string value for the comparison
3979
     *
3980
     * @return int
3981
     */
3982 21
    private function strcmpLowercaseFirst($str1, $str2)
3983
    {
3984 21
        $inversedStr1 = Shared\StringHelper::strCaseReverse($str1);
3985 21
        $inversedStr2 = Shared\StringHelper::strCaseReverse($str2);
3986
3987 21
        return strcmp($inversedStr1, $inversedStr2);
3988
    }
3989
3990
    /**
3991
     * @param mixed $operand1
3992
     * @param mixed $operand2
3993
     * @param mixed $operation
3994
     * @param string $matrixFunction
3995
     * @param mixed $stack
3996
     *
3997
     * @return bool
3998
     */
3999 31
    private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack)
4000
    {
4001
        //    Validate the two operands
4002 31
        if (!$this->validateBinaryOperand($operand1, $stack)) {
4003 2
            return false;
4004
        }
4005 30
        if (!$this->validateBinaryOperand($operand2, $stack)) {
4006 1
            return false;
4007
        }
4008
4009
        //    If either of the operands is a matrix, we need to treat them both as matrices
4010
        //        (converting the other operand to a matrix if need be); then perform the required
4011
        //        matrix operation
4012 30
        if ((is_array($operand1)) || (is_array($operand2))) {
4013
            //    Ensure that both operands are arrays/matrices of the same size
4014
            self::checkMatrixOperands($operand1, $operand2, 2);
4015
4016
            try {
4017
                //    Convert operand 1 from a PHP array to a matrix
4018
                $matrix = new Shared\JAMA\Matrix($operand1);
4019
                //    Perform the required operation against the operand 1 matrix, passing in operand 2
4020
                $matrixResult = $matrix->$matrixFunction($operand2);
4021
                $result = $matrixResult->getArray();
4022
            } catch (\Exception $ex) {
4023
                $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage());
4024
                $result = '#VALUE!';
4025
            }
4026
        } else {
4027 30
            if ((Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) &&
4028 30
                ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) ||
4029 30
                 (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0))) {
4030
                $result = Functions::VALUE();
4031
            } else {
4032
                //    If we're dealing with non-matrix operations, execute the necessary operation
4033
                switch ($operation) {
4034
                    //    Addition
4035 30
                    case '+':
4036 22
                        $result = $operand1 + $operand2;
4037
4038 22
                        break;
4039
                    //    Subtraction
4040 26
                    case '-':
4041 7
                        $result = $operand1 - $operand2;
4042
4043 7
                        break;
4044
                    //    Multiplication
4045 24
                    case '*':
4046 19
                        $result = $operand1 * $operand2;
4047
4048 19
                        break;
4049
                    //    Division
4050 8
                    case '/':
4051 8
                        if ($operand2 == 0) {
4052
                            //    Trap for Divide by Zero error
4053 8
                            $stack->push('Value', '#DIV/0!');
4054 8
                            $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails('#DIV/0!'));
4055
4056 8
                            return false;
4057
                        }
4058
                            $result = $operand1 / $operand2;
4059
4060
                        break;
4061
                    //    Power
4062
                    case '^':
4063
                        $result = pow($operand1, $operand2);
4064
4065
                        break;
4066
                }
4067
            }
4068
        }
4069
4070
        //    Log the result details
4071 25
        $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
4072
        //    And push the result onto the stack
4073 25
        $stack->push('Value', $result);
4074
4075 25
        return true;
4076
    }
4077
4078
    // trigger an error, but nicely, if need be
4079
    protected function raiseFormulaError($errorMessage)
4080
    {
4081
        $this->formulaError = $errorMessage;
4082
        $this->cyclicReferenceStack->clear();
4083
        if (!$this->suppressFormulaErrors) {
4084
            throw new Exception($errorMessage);
4085
        }
4086
        trigger_error($errorMessage, E_USER_ERROR);
4087
    }
4088
4089
    /**
4090
     * Extract range values.
4091
     *
4092
     * @param string &$pRange String based range representation
4093
     * @param Worksheet $pSheet Worksheet
4094
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4095
     *
4096
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4097
     */
4098 60
    public function extractCellRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4099
    {
4100
        // Return value
4101 60
        $returnValue = [];
4102
4103 60
        if ($pSheet !== null) {
4104 60
            $pSheetName = $pSheet->getTitle();
4105 60
            if (strpos($pRange, '!') !== false) {
4106
                list($pSheetName, $pRange) = Worksheet::extractSheetTitle($pRange, true);
4107
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4108
            }
4109
4110
            // Extract range
4111 60
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4112 60
            $pRange = $pSheetName . '!' . $pRange;
4113 60
            if (!isset($aReferences[1])) {
4114
                //    Single cell in range
4115 60
                sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
4116 60
                if ($pSheet->cellExists($aReferences[0])) {
4117 60
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4118
                } else {
4119 60
                    $returnValue[$currentRow][$currentCol] = null;
4120
                }
4121
            } else {
4122
                // Extract cell data for all cells in the range
4123 52
                foreach ($aReferences as $reference) {
4124
                    // Extract range
4125 52
                    sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
4126 52
                    if ($pSheet->cellExists($reference)) {
4127 52
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4128
                    } else {
4129 52
                        $returnValue[$currentRow][$currentCol] = null;
4130
                    }
4131
                }
4132
            }
4133
        }
4134
4135 60
        return $returnValue;
4136
    }
4137
4138
    /**
4139
     * Extract range values.
4140
     *
4141
     * @param string &$pRange String based range representation
4142
     * @param Worksheet $pSheet Worksheet
4143
     * @param bool $resetLog Flag indicating whether calculation log should be reset or not
4144
     *
4145
     * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
4146
     */
4147 5
    public function extractNamedRange(&$pRange = 'A1', Worksheet $pSheet = null, $resetLog = true)
4148
    {
4149
        // Return value
4150 5
        $returnValue = [];
4151
4152 5
        if ($pSheet !== null) {
4153 5
            $pSheetName = $pSheet->getTitle();
4154 5
            if (strpos($pRange, '!') !== false) {
4155
                list($pSheetName, $pRange) = Worksheet::extractSheetTitle($pRange, true);
4156
                $pSheet = $this->spreadsheet->getSheetByName($pSheetName);
4157
            }
4158
4159
            // Named range?
4160 5
            $namedRange = NamedRange::resolveRange($pRange, $pSheet);
4161 5
            if ($namedRange !== null) {
4162 2
                $pSheet = $namedRange->getWorksheet();
4163 2
                $pRange = $namedRange->getRange();
4164 2
                $splitRange = Coordinate::splitRange($pRange);
4165
                //    Convert row and column references
4166 2
                if (ctype_alpha($splitRange[0][0])) {
4167
                    $pRange = $splitRange[0][0] . '1:' . $splitRange[0][1] . $namedRange->getWorksheet()->getHighestRow();
4168 2
                } elseif (ctype_digit($splitRange[0][0])) {
4169 2
                    $pRange = 'A' . $splitRange[0][0] . ':' . $namedRange->getWorksheet()->getHighestColumn() . $splitRange[0][1];
4170
                }
4171
            } else {
4172 3
                return Functions::REF();
4173
            }
4174
4175
            // Extract range
4176 2
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
4177 2
            if (!isset($aReferences[1])) {
4178
                //    Single cell (or single column or row) in range
4179 1
                list($currentCol, $currentRow) = Coordinate::coordinateFromString($aReferences[0]);
4180 1
                if ($pSheet->cellExists($aReferences[0])) {
4181 1
                    $returnValue[$currentRow][$currentCol] = $pSheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
4182
                } else {
4183 1
                    $returnValue[$currentRow][$currentCol] = null;
4184
                }
4185
            } else {
4186
                // Extract cell data for all cells in the range
4187 1
                foreach ($aReferences as $reference) {
4188
                    // Extract range
4189 1
                    list($currentCol, $currentRow) = Coordinate::coordinateFromString($reference);
4190 1
                    if ($pSheet->cellExists($reference)) {
4191 1
                        $returnValue[$currentRow][$currentCol] = $pSheet->getCell($reference)->getCalculatedValue($resetLog);
4192
                    } else {
4193 1
                        $returnValue[$currentRow][$currentCol] = null;
4194
                    }
4195
                }
4196
            }
4197
        }
4198
4199 2
        return $returnValue;
4200
    }
4201
4202
    /**
4203
     * Is a specific function implemented?
4204
     *
4205
     * @param string $pFunction Function Name
4206
     *
4207
     * @return bool
4208
     */
4209 3
    public function isImplemented($pFunction)
4210
    {
4211 3
        $pFunction = strtoupper($pFunction);
4212 3
        $notImplemented = !isset(self::$phpSpreadsheetFunctions[$pFunction]) || (is_array(self::$phpSpreadsheetFunctions[$pFunction]['functionCall']) && self::$phpSpreadsheetFunctions[$pFunction]['functionCall'][1] === 'DUMMY');
4213
4214 3
        return !$notImplemented;
4215
    }
4216
4217
    /**
4218
     * Get a list of all implemented functions as an array of function objects.
4219
     *
4220
     * @return array of Category
4221
     */
4222
    public function getFunctions()
4223
    {
4224
        return self::$phpSpreadsheetFunctions;
4225
    }
4226
4227
    /**
4228
     * Get a list of implemented Excel function names.
4229
     *
4230
     * @return array
4231
     */
4232 2
    public function getImplementedFunctionNames()
4233
    {
4234 2
        $returnValue = [];
4235 2
        foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
4236 2
            if ($this->isImplemented($functionName)) {
4237 2
                $returnValue[] = $functionName;
4238
            }
4239
        }
4240
4241 2
        return $returnValue;
4242
    }
4243
}
4244